activerecord-refined 0.3.1 → 0.3.3

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,90 @@
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
+ # == and != mean SQL = and <>, and = NULL is never true there, so nil
9
+ # is rejected rather than silently rewritten to IS NULL. null? builds
10
+ # its node directly and stays clear of this check.
11
+ def ==(other)
12
+ if other.nil?
13
+ raise ArgumentError, "== does not take nil; use null? instead"
14
+ end
15
+ Comparison.new(self, :==, other)
16
+ end
17
+
18
+ def !=(other)
19
+ if other.nil?
20
+ raise ArgumentError, "!= does not take nil; use !null? instead"
21
+ end
22
+ Comparison.new(self, :!=, other)
23
+ end
24
+
25
+ def >(other)
26
+ Comparison.new(self, :>, other)
27
+ end
28
+
29
+ def >=(other)
30
+ Comparison.new(self, :>=, other)
31
+ end
32
+
33
+ def <(other)
34
+ Comparison.new(self, :<, other)
35
+ end
36
+
37
+ def <=(other)
38
+ Comparison.new(self, :<=, other)
39
+ end
40
+
41
+ def =~(pattern)
42
+ Match.new(self, pattern)
43
+ end
44
+
45
+ def !~(pattern)
46
+ Match.new(self, pattern, negated: true)
47
+ end
48
+
49
+ def null?
50
+ Comparison.new(self, :==, nil)
51
+ end
52
+
53
+ def in?(values)
54
+ In.new(self, values)
55
+ end
56
+
57
+ def between?(min, max)
58
+ In.new(self, min..max)
59
+ end
60
+
61
+ def like?(pattern)
62
+ Like.new(self, pattern)
63
+ end
64
+
65
+ def start_with?(*prefixes)
66
+ if prefixes.empty?
67
+ raise ArgumentError, "start_with? needs at least one prefix"
68
+ end
69
+ Like.any(self, prefixes.map {|prefix| "#{Like.escape(prefix)}%" })
70
+ end
71
+
72
+ def end_with?(*suffixes)
73
+ if suffixes.empty?
74
+ raise ArgumentError, "end_with? needs at least one suffix"
75
+ end
76
+ Like.any(self, suffixes.map {|suffix| "%#{Like.escape(suffix)}" })
77
+ end
78
+
79
+ def include?(substring)
80
+ Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
81
+ end
82
+
83
+ def member?(element)
84
+ Member.new(self, element)
85
+ end
86
+ end
87
+
4
88
  class Node
5
89
  def to_arel(table)
6
90
  raise ScriptError, "subclass must override this method"
@@ -17,6 +101,18 @@ module ActiveRecord
17
101
  def desc
18
102
  Ordering.new(self, :desc)
19
103
  end
104
+
105
+ private
106
+
107
+ # Resolves an operand denoting a column or an expression.
108
+ def to_arel_operand(operand, table)
109
+ case operand
110
+ when Node then operand.to_arel(table)
111
+ when :* then Arel.star
112
+ when Symbol then table[operand]
113
+ else operand
114
+ end
115
+ end
20
116
  end
21
117
 
22
118
  class Predicate < Node
@@ -34,6 +130,8 @@ module ActiveRecord
34
130
  end
35
131
 
36
132
  class Column < Node
133
+ include Predications
134
+
37
135
  attr_reader :table_name, :column_name
38
136
 
39
137
  def initialize(table_name, column_name)
@@ -45,20 +143,14 @@ module ActiveRecord
45
143
  Arel::Table.new(table_name)[column_name]
46
144
  end
47
145
 
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
146
  %i[count sum average maximum minimum].each do |func|
57
147
  define_method(func) { Aggregate.new(self, func) }
58
148
  end
59
149
  end
60
150
 
61
151
  class Aggregate < Node
152
+ include Predications
153
+
62
154
  attr_reader :operand, :function
63
155
 
64
156
  def initialize(operand, function)
@@ -67,15 +159,7 @@ module ActiveRecord
67
159
  end
68
160
 
69
161
  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) }
162
+ to_arel_operand(operand, table).public_send(function)
79
163
  end
80
164
  end
81
165
 
@@ -88,12 +172,7 @@ module ActiveRecord
88
172
  end
89
173
 
90
174
  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)
175
+ to_arel_operand(operand, table).as(alias_name.to_s)
97
176
  end
98
177
  end
99
178
 
@@ -106,16 +185,13 @@ module ActiveRecord
106
185
  end
107
186
 
108
187
  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)
188
+ to_arel_operand(operand, table).public_send(direction)
115
189
  end
116
190
  end
117
191
 
118
192
  class Function < Node
193
+ include Predications
194
+
119
195
  attr_reader :name, :args
120
196
 
121
197
  def initialize(name, args)
@@ -126,22 +202,20 @@ module ActiveRecord
126
202
  def to_arel(table)
127
203
  arel_args = args.map do |arg|
128
204
  case arg
129
- when Node then arg.to_arel(table)
130
- when Symbol then table[arg]
205
+ when Node, Symbol then to_arel_operand(arg, table)
131
206
  else Arel::Nodes.build_quoted(arg)
132
207
  end
133
208
  end
134
209
  Arel::Nodes::NamedFunction.new(name, arel_args)
135
210
  end
136
-
137
- %i[== != =~ !~ > >= < <=].each do |op|
138
- define_method(op) {|val| Comparison.new(self, op, val) }
139
- end
140
211
  end
141
212
 
213
+ # A plain SQL comparison. The value is passed through as it is, so a Range
214
+ # or an Array compares against a PostgreSQL range or array column, the way
215
+ # ActiveRecord's own force_equality? types do.
142
216
  class Comparison < Predicate
143
217
  OPERATOR_MAP = {
144
- :== => :eq, :!= => :not_eq, :=~ => :matches, :!~ => :does_not_match,
218
+ :== => :eq, :!= => :not_eq,
145
219
  :> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
146
220
  }.freeze
147
221
 
@@ -154,26 +228,173 @@ module ActiveRecord
154
228
  end
155
229
 
156
230
  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)
231
+ arel_column = to_arel_operand(column, table)
232
+ arel_value = value.is_a?(Node) ? value.to_arel(table) : value
233
+ arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
234
+ end
235
+ end
236
+
237
+ # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
238
+ # relation.
239
+ class In < Predicate
240
+ attr_reader :operand, :values
241
+
242
+ def initialize(operand, values)
243
+ @operand = operand
244
+ @values = values
245
+ end
246
+
247
+ def to_arel(table)
248
+ arel_operand = to_arel_operand(operand, table)
249
+ case values
250
+ when Range then arel_operand.between(values)
251
+ when ActiveRecord::Relation then arel_operand.in(subquery(values))
252
+ else arel_operand.in(values)
253
+ end
254
+ end
255
+
256
+ private
257
+
258
+ # The same treatment ActiveRecord's own RelationHandler gives a
259
+ # relation used as a value: without an explicit select list the
260
+ # subquery selects the model's primary key.
261
+ def subquery(relation)
262
+ if relation.eager_loading?
263
+ relation = relation.send(:apply_join_dependency)
264
+ end
265
+ if relation.select_values.empty?
266
+ model = relation.model
267
+ if model.composite_primary_key?
268
+ raise ArgumentError,
269
+ "Cannot map composite primary key #{model.primary_key} to IN"
270
+ end
271
+ relation = relation.select(relation.table[model.primary_key])
272
+ end
273
+ relation.arel
274
+ end
275
+ end
276
+
277
+ # EXISTS (SELECT ...) for a relation. Correlate the subquery with the
278
+ # outer table through qualified columns. EXISTS only asks whether a row
279
+ # comes back, so unlike In there is no select list to fix up.
280
+ class Exists < Predicate
281
+ attr_reader :relation
282
+
283
+ def initialize(relation)
284
+ @relation = relation
285
+ end
286
+
287
+ def to_arel(_table)
288
+ subquery = relation
289
+ if subquery.eager_loading?
290
+ subquery = subquery.send(:apply_join_dependency)
291
+ end
292
+ subquery.arel.exists
293
+ end
294
+ end
295
+
296
+ class Like < Predicate
297
+ ESCAPE = "\\".freeze
298
+
299
+ # Escapes % and _ so that they match literally. The pattern built from
300
+ # the result must be used with ESCAPE, since SQLite has no default
301
+ # escape character.
302
+ def self.escape(string)
303
+ ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
304
+ end
305
+
306
+ # ORs one LIKE per pattern, for the shortcuts that accept several
307
+ # literals the way String#start_with? does.
308
+ def self.any(operand, patterns)
309
+ patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
310
+ inject {|left, right| Or.new(left, right) }
311
+ end
312
+
313
+ attr_reader :operand, :pattern, :escape
314
+
315
+ def initialize(operand, pattern, escape = nil)
316
+ @operand = operand
317
+ @pattern = pattern
318
+ @escape = escape
319
+ end
320
+
321
+ def to_arel(table)
322
+ # Arel matches case-insensitively unless told otherwise, which turns
323
+ # into ILIKE on PostgreSQL. like? means SQL LIKE on every adapter.
324
+ to_arel_operand(operand, table).matches(pattern, escape, true)
325
+ end
326
+ end
327
+
328
+ # Containment in a PostgreSQL array column. The two flavors of "does it
329
+ # contain this?" split by name the way Ruby's own classes do: include? is
330
+ # String's substring match (LIKE), member? is Enumerable's element test,
331
+ # which String does not have. The elements are rendered as an array
332
+ # literal, which PostgreSQL coerces to the column's element type, so any
333
+ # expression works as the operand and no schema lookup is needed.
334
+ class Member < Predicate
335
+ attr_reader :operand, :elements
336
+
337
+ def initialize(operand, element)
338
+ @operand = operand
339
+ @elements = element.is_a?(::Array) ? element : [element]
340
+ end
341
+
342
+ def to_arel(table)
343
+ arel_operand = to_arel_operand(operand, table)
344
+ arel_operand.contains(Arel::Nodes.build_quoted(array_literal))
345
+ end
346
+
347
+ private
348
+
349
+ # PostgreSQL array input syntax: elements joined by commas inside
350
+ # braces, and an element is double-quoted whenever it is empty, spells
351
+ # NULL, or contains a character the parser treats specially.
352
+ def array_literal
353
+ encoded = elements.map do |value|
354
+ s = value.to_s
355
+ if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
356
+ "\"#{s.gsub(/["\\]/) {|c| "\\#{c}" }}\""
357
+ else
358
+ s
359
+ end
360
+ end
361
+ "{#{encoded.join(',')}}"
362
+ end
363
+ end
364
+
365
+ # Regular expression match: REGEXP on MySQL, ~ on PostgreSQL. SQLite has
366
+ # no regexp operator built in, so Arel raises NotImplementedError there.
367
+ class Match < Predicate
368
+ attr_reader :operand, :pattern, :negated
369
+
370
+ def initialize(operand, pattern, negated: false)
371
+ @operand = operand
372
+ @pattern = pattern.is_a?(Regexp) ? regexp_source(pattern) : pattern
373
+ @negated = negated
374
+ end
375
+
376
+ def to_arel(table)
377
+ arel_operand = to_arel_operand(operand, table)
378
+ if negated
379
+ arel_operand.does_not_match_regexp(pattern)
174
380
  else
175
- arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
381
+ arel_operand.matches_regexp(pattern)
382
+ end
383
+ end
384
+
385
+ private
386
+
387
+ # A Regexp literal reads naturally with =~, but only its source crosses
388
+ # over; the database has its own dialect and no notion of Ruby's flags.
389
+ # Dropping a flag would silently change what the query matches, so
390
+ # anything beyond a plain literal is refused rather than ignored.
391
+ def regexp_source(regexp)
392
+ unless regexp.options.zero?
393
+ raise ArgumentError,
394
+ "#{regexp.inspect} has options that SQL cannot express; " \
395
+ "pass the pattern as a string instead"
176
396
  end
397
+ regexp.source
177
398
  end
178
399
  end
179
400
 
@@ -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) }
@@ -46,6 +40,10 @@ module ActiveRecord
46
40
  SCALAR_FUNCTIONS.each do |name|
47
41
  define_method(name) {|*args| AST::Function.new(name.to_s.upcase, args) }
48
42
  end
43
+
44
+ def exists?(relation)
45
+ AST::Exists.new(relation)
46
+ end
49
47
  end
50
48
 
51
49
  module QueryMethods
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Refined
3
- VERSION = '0.3.1'
3
+ VERSION = '0.3.3'
4
4
  end
5
5
  end