activerecord-refined 0.3.0 → 0.3.2

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.
@@ -1,6 +1,71 @@
1
1
  module ActiveRecord
2
2
  module Refined
3
3
  module AST
4
+ # Predicate builders shared by symbols, qualified columns and
5
+ # expressions. Imported into the Symbol refinement with
6
+ # Refinement#import_methods, so every method must be defined with def.
7
+ module Predications
8
+ def ==(other)
9
+ Comparison.new(self, :==, other)
10
+ end
11
+
12
+ def !=(other)
13
+ Comparison.new(self, :!=, other)
14
+ end
15
+
16
+ def >(other)
17
+ Comparison.new(self, :>, other)
18
+ end
19
+
20
+ def >=(other)
21
+ Comparison.new(self, :>=, other)
22
+ end
23
+
24
+ def <(other)
25
+ Comparison.new(self, :<, other)
26
+ end
27
+
28
+ def <=(other)
29
+ Comparison.new(self, :<=, other)
30
+ end
31
+
32
+ def =~(pattern)
33
+ Match.new(self, pattern)
34
+ end
35
+
36
+ def !~(pattern)
37
+ Match.new(self, pattern, negated: true)
38
+ end
39
+
40
+ def null?
41
+ Comparison.new(self, :==, nil)
42
+ end
43
+
44
+ def in?(values)
45
+ In.new(self, values)
46
+ end
47
+
48
+ def between?(min, max)
49
+ In.new(self, min..max)
50
+ end
51
+
52
+ def like?(pattern)
53
+ Like.new(self, pattern)
54
+ end
55
+
56
+ def start_with?(prefix)
57
+ Like.new(self, "#{Like.escape(prefix)}%", Like::ESCAPE)
58
+ end
59
+
60
+ def end_with?(suffix)
61
+ Like.new(self, "%#{Like.escape(suffix)}", Like::ESCAPE)
62
+ end
63
+
64
+ def include?(substring)
65
+ Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
66
+ end
67
+ end
68
+
4
69
  class Node
5
70
  def to_arel(table)
6
71
  raise ScriptError, "subclass must override this method"
@@ -17,6 +82,18 @@ module ActiveRecord
17
82
  def desc
18
83
  Ordering.new(self, :desc)
19
84
  end
85
+
86
+ private
87
+
88
+ # Resolves an operand denoting a column or an expression.
89
+ def to_arel_operand(operand, table)
90
+ case operand
91
+ when Node then operand.to_arel(table)
92
+ when :* then Arel.star
93
+ when Symbol then table[operand]
94
+ else operand
95
+ end
96
+ end
20
97
  end
21
98
 
22
99
  class Predicate < Node
@@ -34,6 +111,8 @@ module ActiveRecord
34
111
  end
35
112
 
36
113
  class Column < Node
114
+ include Predications
115
+
37
116
  attr_reader :table_name, :column_name
38
117
 
39
118
  def initialize(table_name, column_name)
@@ -45,20 +124,14 @@ module ActiveRecord
45
124
  Arel::Table.new(table_name)[column_name]
46
125
  end
47
126
 
48
- %i[== != =~ !~ > >= < <=].each do |op|
49
- define_method(op) {|val| Comparison.new(self, op, val) }
50
- end
51
-
52
- def null?
53
- Comparison.new(self, :==, nil)
54
- end
55
-
56
127
  %i[count sum average maximum minimum].each do |func|
57
128
  define_method(func) { Aggregate.new(self, func) }
58
129
  end
59
130
  end
60
131
 
61
132
  class Aggregate < Node
133
+ include Predications
134
+
62
135
  attr_reader :operand, :function
63
136
 
64
137
  def initialize(operand, function)
@@ -67,15 +140,7 @@ module ActiveRecord
67
140
  end
68
141
 
69
142
  def to_arel(table)
70
- arel_operand = case operand
71
- when Node then operand.to_arel(table)
72
- else table[operand]
73
- end
74
- arel_operand.public_send(function)
75
- end
76
-
77
- %i[== != =~ !~ > >= < <=].each do |op|
78
- define_method(op) {|val| Comparison.new(self, op, val) }
143
+ to_arel_operand(operand, table).public_send(function)
79
144
  end
80
145
  end
81
146
 
@@ -88,12 +153,7 @@ module ActiveRecord
88
153
  end
89
154
 
90
155
  def to_arel(table)
91
- arel_operand = case operand
92
- when Node then operand.to_arel(table)
93
- when Symbol then table[operand]
94
- else operand
95
- end
96
- arel_operand.as(alias_name.to_s)
156
+ to_arel_operand(operand, table).as(alias_name.to_s)
97
157
  end
98
158
  end
99
159
 
@@ -106,16 +166,13 @@ module ActiveRecord
106
166
  end
107
167
 
108
168
  def to_arel(table)
109
- arel_operand = case operand
110
- when Node then operand.to_arel(table)
111
- when Symbol then table[operand]
112
- else operand
113
- end
114
- arel_operand.public_send(direction)
169
+ to_arel_operand(operand, table).public_send(direction)
115
170
  end
116
171
  end
117
172
 
118
173
  class Function < Node
174
+ include Predications
175
+
119
176
  attr_reader :name, :args
120
177
 
121
178
  def initialize(name, args)
@@ -126,22 +183,20 @@ module ActiveRecord
126
183
  def to_arel(table)
127
184
  arel_args = args.map do |arg|
128
185
  case arg
129
- when Node then arg.to_arel(table)
130
- when Symbol then table[arg]
186
+ when Node, Symbol then to_arel_operand(arg, table)
131
187
  else Arel::Nodes.build_quoted(arg)
132
188
  end
133
189
  end
134
190
  Arel::Nodes::NamedFunction.new(name, arel_args)
135
191
  end
136
-
137
- %i[== != =~ !~ > >= < <=].each do |op|
138
- define_method(op) {|val| Comparison.new(self, op, val) }
139
- end
140
192
  end
141
193
 
194
+ # A plain SQL comparison. The value is passed through as it is, so a Range
195
+ # or an Array compares against a PostgreSQL range or array column, the way
196
+ # ActiveRecord's own force_equality? types do.
142
197
  class Comparison < Predicate
143
198
  OPERATOR_MAP = {
144
- :== => :eq, :!= => :not_eq, :=~ => :matches, :!~ => :does_not_match,
199
+ :== => :eq, :!= => :not_eq,
145
200
  :> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
146
201
  }.freeze
147
202
 
@@ -154,26 +209,88 @@ module ActiveRecord
154
209
  end
155
210
 
156
211
  def to_arel(table)
157
- arel_column = case column
158
- when Node then column.to_arel(table)
159
- else table[column]
160
- end
161
- arel_value = case value
162
- when Node then value.to_arel(table)
163
- else value
164
- end
165
- case
166
- when operator == :== && Range === value
167
- arel_column.between(value)
168
- when operator == :!= && Range === value
169
- arel_column.not_between(value)
170
- when operator == :== && Array === value
171
- arel_column.in(value)
172
- when operator == :!= && Array === value
173
- arel_column.not_in(value)
212
+ arel_column = to_arel_operand(column, table)
213
+ arel_value = value.is_a?(Node) ? value.to_arel(table) : value
214
+ arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
215
+ end
216
+ end
217
+
218
+ # IN for a list of values, BETWEEN for a range.
219
+ class In < Predicate
220
+ attr_reader :operand, :values
221
+
222
+ def initialize(operand, values)
223
+ @operand = operand
224
+ @values = values
225
+ end
226
+
227
+ def to_arel(table)
228
+ arel_operand = to_arel_operand(operand, table)
229
+ case values
230
+ when Range then arel_operand.between(values)
231
+ else arel_operand.in(values)
232
+ end
233
+ end
234
+ end
235
+
236
+ class Like < Predicate
237
+ ESCAPE = "\\".freeze
238
+
239
+ # Escapes % and _ so that they match literally. The pattern built from
240
+ # the result must be used with ESCAPE, since SQLite has no default
241
+ # escape character.
242
+ def self.escape(string)
243
+ ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
244
+ end
245
+
246
+ attr_reader :operand, :pattern, :escape
247
+
248
+ def initialize(operand, pattern, escape = nil)
249
+ @operand = operand
250
+ @pattern = pattern
251
+ @escape = escape
252
+ end
253
+
254
+ def to_arel(table)
255
+ # Arel matches case-insensitively unless told otherwise, which turns
256
+ # into ILIKE on PostgreSQL. like? means SQL LIKE on every adapter.
257
+ to_arel_operand(operand, table).matches(pattern, escape, true)
258
+ end
259
+ end
260
+
261
+ # Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
262
+ # no regexp operator built in, so Arel raises NotImplementedError there.
263
+ class Match < Predicate
264
+ attr_reader :operand, :pattern, :negated
265
+
266
+ def initialize(operand, pattern, negated: false)
267
+ @operand = operand
268
+ @pattern = pattern.is_a?(Regexp) ? regexp_source(pattern) : pattern
269
+ @negated = negated
270
+ end
271
+
272
+ def to_arel(table)
273
+ arel_operand = to_arel_operand(operand, table)
274
+ if negated
275
+ arel_operand.does_not_match_regexp(pattern)
174
276
  else
175
- arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
277
+ arel_operand.matches_regexp(pattern)
278
+ end
279
+ end
280
+
281
+ private
282
+
283
+ # A Regexp literal reads naturally with =~, but only its source crosses
284
+ # over; the database has its own dialect and no notion of Ruby's flags.
285
+ # Dropping a flag would silently change what the query matches, so
286
+ # anything beyond a plain literal is refused rather than ignored.
287
+ def regexp_source(regexp)
288
+ unless regexp.options.zero?
289
+ raise ArgumentError,
290
+ "#{regexp.inspect} has options that SQL cannot express; " \
291
+ "pass the pattern as a string instead"
176
292
  end
293
+ regexp.source
177
294
  end
178
295
  end
179
296
 
@@ -2,13 +2,7 @@ module ActiveRecord
2
2
  module Refined
3
3
  module BlockSyntax
4
4
  refine Symbol do
5
- %i[== != =~ !~ > >= < <=].each do |op|
6
- define_method(op) {|val| AST::Comparison.new(self, op, val) }
7
- end
8
-
9
- def null?
10
- AST::Comparison.new(self, :==, nil)
11
- end
5
+ import_methods AST::Predications
12
6
 
13
7
  %i[count sum average maximum minimum].each do |func|
14
8
  define_method(func) { AST::Aggregate.new(self, func) }
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Refined
3
- VERSION = '0.3.0'
3
+ VERSION = '0.3.2'
4
4
  end
5
5
  end