database_consistency 2.1.3 → 3.0.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/lib/database_consistency/checkers/association_checkers/foreign_key_cascade_checker.rb +5 -4
- data/lib/database_consistency/checkers/association_checkers/missing_dependent_destroy_checker.rb +1 -1
- data/lib/database_consistency/checkers/column_checkers/missing_index_find_by_checker.rb +187 -0
- data/lib/database_consistency/checkers/index_checkers/unique_index_checker.rb +7 -4
- data/lib/database_consistency/checkers/model_checkers/view_primary_key_checker.rb +36 -0
- data/lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb +7 -3
- data/lib/database_consistency/files_helper.rb +52 -0
- data/lib/database_consistency/helper.rb +32 -0
- data/lib/database_consistency/prism_helper.rb +38 -0
- data/lib/database_consistency/version.rb +1 -1
- data/lib/database_consistency/writers/simple/missing_index_find_by.rb +32 -0
- data/lib/database_consistency/writers/simple/view_missing_primary_key.rb +21 -0
- data/lib/database_consistency/writers/simple/view_primary_key_column_missing.rb +21 -0
- data/lib/database_consistency.rb +7 -0
- metadata +13 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d92e907cbb8ca8c49b6b4015614f9bc55074d37cb96130b97e62ef5266211d1
|
|
4
|
+
data.tar.gz: c97b5ff7fb08271812b489ea695f20b0d45c56f5109bd1ec246e1704917b33cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 004c32a1588bc7cb59299352e09a1866f20229735d56b1290d337ce2322ecd4ff20918e817e26118461356a47b6a81580ede89f695be2ec9fdf2739f44a38064
|
|
7
|
+
data.tar.gz: 52d25e2ee4da71b6130d442a2443c7f3e02ad84674f7d19d67270356ebe28889db57581907d5d65cc9579013fb9d3b073461e210f0320a90db14cad8bab5e024
|
|
@@ -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|
|
|
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.
|
|
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.
|
|
83
|
+
foreign_key: Helper.extract_columns(association.foreign_key).join('+'),
|
|
83
84
|
cascade_option: required_foreign_key_cascade,
|
|
84
85
|
**report_attributes
|
|
85
86
|
)
|
|
@@ -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?
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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?
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 occurrences" : ''
|
|
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
|
|
@@ -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 pointing to a 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 pointing to a view has a non-existent primary_key column 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
|
data/lib/database_consistency.rb
CHANGED
|
@@ -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:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evgeniy Demin
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-02 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.
|
|
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.
|