database_consistency 2.1.2 → 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/association_checker.rb +2 -2
- data/lib/database_consistency/checkers/association_checkers/foreign_key_cascade_checker.rb +5 -4
- data/lib/database_consistency/checkers/association_checkers/foreign_key_checker.rb +1 -1
- data/lib/database_consistency/checkers/association_checkers/missing_dependent_destroy_checker.rb +1 -8
- 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 +14 -6
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
|
|
@@ -24,8 +24,8 @@ module DatabaseConsistency
|
|
|
24
24
|
@table_or_model_name ||= model.name.to_s
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
model.connection.foreign_keys(model.table_name).
|
|
27
|
+
def foreign_key # rubocop:disable Metrics/AbcSize
|
|
28
|
+
model.connection.foreign_keys(model.table_name).find do |foreign_key|
|
|
29
29
|
(Helper.extract_columns(association.foreign_key) - Array.wrap(foreign_key.column)).empty? &&
|
|
30
30
|
foreign_key.to_table == association.klass.table_name
|
|
31
31
|
end
|
|
@@ -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
|
)
|
data/lib/database_consistency/checkers/association_checkers/missing_dependent_destroy_checker.rb
CHANGED
|
@@ -15,7 +15,7 @@ module DatabaseConsistency
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def preconditions
|
|
18
|
-
association.belongs_to? &&
|
|
18
|
+
association.belongs_to? && !association.polymorphic? && foreign_key
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def check
|
|
@@ -34,13 +34,6 @@ module DatabaseConsistency
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def foreign_key
|
|
38
|
-
association.klass
|
|
39
|
-
.connection
|
|
40
|
-
.foreign_keys(model.table_name)
|
|
41
|
-
.find { |fk| fk.column == association.foreign_key.to_s }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
37
|
def cascade?
|
|
45
38
|
%i[cascade nullify].include? foreign_key.options[:on_delete]
|
|
46
39
|
end
|
|
@@ -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,14 +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
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02
|
|
11
|
+
date: 2026-03-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -136,7 +136,7 @@ dependencies:
|
|
|
136
136
|
- - ">"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '1.3'
|
|
139
|
-
description:
|
|
139
|
+
description:
|
|
140
140
|
email:
|
|
141
141
|
- lawliet.djez@gmail.com
|
|
142
142
|
executables:
|
|
@@ -158,6 +158,7 @@ files:
|
|
|
158
158
|
- lib/database_consistency/checkers/column_checkers/enum_value_checker.rb
|
|
159
159
|
- lib/database_consistency/checkers/column_checkers/implicit_ordering_checker.rb
|
|
160
160
|
- lib/database_consistency/checkers/column_checkers/length_constraint_checker.rb
|
|
161
|
+
- lib/database_consistency/checkers/column_checkers/missing_index_find_by_checker.rb
|
|
161
162
|
- lib/database_consistency/checkers/column_checkers/null_constraint_checker.rb
|
|
162
163
|
- lib/database_consistency/checkers/column_checkers/primary_key_type_checker.rb
|
|
163
164
|
- lib/database_consistency/checkers/column_checkers/three_state_boolean_checker.rb
|
|
@@ -169,6 +170,7 @@ files:
|
|
|
169
170
|
- lib/database_consistency/checkers/index_checkers/unique_index_checker.rb
|
|
170
171
|
- lib/database_consistency/checkers/model_checkers/missing_table_checker.rb
|
|
171
172
|
- lib/database_consistency/checkers/model_checkers/model_checker.rb
|
|
173
|
+
- lib/database_consistency/checkers/model_checkers/view_primary_key_checker.rb
|
|
172
174
|
- lib/database_consistency/checkers/validator_checkers/case_sensitive_unique_validation_checker.rb
|
|
173
175
|
- lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb
|
|
174
176
|
- lib/database_consistency/checkers/validator_checkers/validator_checker.rb
|
|
@@ -180,7 +182,9 @@ files:
|
|
|
180
182
|
- lib/database_consistency/databases/types/sqlite.rb
|
|
181
183
|
- lib/database_consistency/debug_context.rb
|
|
182
184
|
- lib/database_consistency/errors.rb
|
|
185
|
+
- lib/database_consistency/files_helper.rb
|
|
183
186
|
- lib/database_consistency/helper.rb
|
|
187
|
+
- lib/database_consistency/prism_helper.rb
|
|
184
188
|
- lib/database_consistency/processors/associations_processor.rb
|
|
185
189
|
- lib/database_consistency/processors/base_processor.rb
|
|
186
190
|
- lib/database_consistency/processors/columns_processor.rb
|
|
@@ -229,6 +233,7 @@ files:
|
|
|
229
233
|
- lib/database_consistency/writers/simple/missing_dependent_destroy.rb
|
|
230
234
|
- lib/database_consistency/writers/simple/missing_foreign_key.rb
|
|
231
235
|
- lib/database_consistency/writers/simple/missing_foreign_key_cascade.rb
|
|
236
|
+
- lib/database_consistency/writers/simple/missing_index_find_by.rb
|
|
232
237
|
- lib/database_consistency/writers/simple/missing_table.rb
|
|
233
238
|
- lib/database_consistency/writers/simple/missing_unique_index.rb
|
|
234
239
|
- lib/database_consistency/writers/simple/missing_uniqueness_validation.rb
|
|
@@ -241,6 +246,8 @@ files:
|
|
|
241
246
|
- lib/database_consistency/writers/simple/redundant_unique_index.rb
|
|
242
247
|
- lib/database_consistency/writers/simple/small_primary_key.rb
|
|
243
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
|
|
244
251
|
- lib/database_consistency/writers/simple_writer.rb
|
|
245
252
|
- lib/database_consistency/writers/todo_writer.rb
|
|
246
253
|
homepage: https://github.com/djezzzl/database_consistency
|
|
@@ -275,9 +282,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
275
282
|
- !ruby/object:Gem::Version
|
|
276
283
|
version: '0'
|
|
277
284
|
requirements: []
|
|
278
|
-
rubygems_version: 3.
|
|
279
|
-
signing_key:
|
|
285
|
+
rubygems_version: 3.4.19
|
|
286
|
+
signing_key:
|
|
280
287
|
specification_version: 4
|
|
281
288
|
summary: Provide an easy way to check the consistency of the database constraints
|
|
282
289
|
with the application validations.
|
|
283
290
|
test_files: []
|
|
291
|
+
...
|