forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathunique_validation_without_index.rb
165 lines (134 loc) · 4.68 KB
/
unique_validation_without_index.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# When you define a uniqueness validation in Active Record model,
# you also should add a unique index for the column. There are two reasons
# First, duplicated records may occur even if Active Record's validation
# is defined.
# Second, it will cause slow queries. The validation executes a `SELECT`
# statement with the target column when inserting/updating a record.
# If the column does not have an index and the table is large,
# the query will be heavy.
#
# Note that the cop does nothing if db/schema.rb does not exist.
#
# @example
# # bad - if the schema does not have a unique index
# validates :account, uniqueness: true
#
# # good - if the schema has a unique index
# validates :account, uniqueness: true
#
# # good - even if the schema does not have a unique index
# validates :account, length: { minimum: MIN_LENGTH }
#
class UniqueValidationWithoutIndex < Base
include ActiveRecordHelper
MSG = 'Uniqueness validation should be with a unique index.'
RESTRICT_ON_SEND = %i[validates].freeze
def on_send(node)
return unless uniqueness_part(node)
return if condition_part?(node)
return unless schema
klass, table, names = find_schema_information(node)
return unless names
return if with_index?(klass, table, names)
add_offense(node)
end
private
def find_schema_information(node)
klass = class_node(node)
return unless klass
table = schema.table_by(name: table_name(klass))
names = column_names(node)
[klass, table, names]
end
def with_index?(klass, table, names)
# Compatibility for Rails 4.2.
add_indicies = schema.add_indicies_by(table_name: table_name(klass))
(table.indices + add_indicies).any? do |index|
index.unique &&
(index.columns.to_set == names ||
include_column_names_in_expression_index?(index, names))
end
end
def include_column_names_in_expression_index?(index, column_names)
return false unless (expression_index = index.expression)
column_names.all? do |column_name|
expression_index.include?(column_name)
end
end
def column_names(node)
arg = node.first_argument
return unless arg.str_type? || arg.sym_type?
ret = [arg.value]
names_from_scope = column_names_from_scope(node)
ret.concat(names_from_scope) if names_from_scope
ret.map! do |name|
klass = class_node(node)
resolve_relation_into_column(
name: name.to_s,
class_node: klass,
table: schema.table_by(name: table_name(klass))
)
end
ret.include?(nil) ? nil : ret.to_set
end
def column_names_from_scope(node)
uniq = uniqueness_part(node)
return unless uniq.hash_type?
scope = find_scope(uniq)
return unless scope
scope = unfreeze_scope(scope)
case scope.type
when :sym, :str
[scope.value]
when :array
array_node_to_array(scope)
end
end
def find_scope(pairs)
pairs.each_pair.find do |pair|
key = pair.key
next unless key.sym_type? && key.value == :scope
break pair.value
end
end
def unfreeze_scope(scope)
scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
end
def class_node(node)
node.each_ancestor.find(&:class_type?)
end
def uniqueness_part(node)
pairs = node.arguments.last
return unless pairs.hash_type?
pairs.each_pair.find do |pair|
next unless pair.key.sym_type? && pair.key.value == :uniqueness
break pair.value
end
end
def condition_part?(node)
pairs = node.arguments.last
return unless pairs.hash_type?
pairs.each_pair.any? do |pair|
key = pair.key
next unless key.sym_type?
key.value == :if || key.value == :unless
end
end
def array_node_to_array(node)
node.values.map do |elm|
case elm.type
when :str, :sym
elm.value
else
return nil
end
end
end
end
end
end
end