activerecord-refined 0.3.3 → 0.5.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.
@@ -1,6 +1,23 @@
1
1
  module ActiveRecord
2
2
  module Refined
3
3
  module AST
4
+ # Column aliases and function names are written into the SQL as given,
5
+ # where a value would have been quoted, so anything that is not a plain
6
+ # name is refused. Quoting them instead would need the model's adapter,
7
+ # which does not reach this far, and quoting with the wrong one would be
8
+ # a bug of its own -- MySQL does not read "x" as an identifier.
9
+ NAME = /[[:alpha:]_][[:alnum:]_$]*/
10
+ ALIAS_NAME = /\A#{NAME}\z/
11
+ FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
12
+ # A SQL type as cast writes it: words, at most parenthesized with
13
+ # lengths -- double precision, decimal(10,2).
14
+ TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
15
+
16
+ def self.check_name(name, pattern, what)
17
+ return name if pattern.match?(name.to_s)
18
+ raise ArgumentError, "#{name.inspect} is not a plain #{what}"
19
+ end
20
+
4
21
  # Predicate builders shared by symbols, qualified columns and
5
22
  # expressions. Imported into the Symbol refinement with
6
23
  # Refinement#import_methods, so every method must be defined with def.
@@ -62,6 +79,30 @@ module ActiveRecord
62
79
  Like.new(self, pattern)
63
80
  end
64
81
 
82
+ def ilike?(pattern)
83
+ Like.new(self, pattern, nil, case_sensitive: false)
84
+ end
85
+
86
+ # Case-insensitive equality, folded on both sides rather than left to
87
+ # the collation, so it means the same thing on every adapter.
88
+ def casecmp?(value)
89
+ if value.nil?
90
+ raise ArgumentError, "casecmp? does not take nil; use null? instead"
91
+ end
92
+ Comparison.new(Function.new("LOWER", [self]), :==,
93
+ Function.new("LOWER", [value]))
94
+ end
95
+
96
+ # Null-safe comparison: unlike = and <>, these treat NULL as a value,
97
+ # so not_distinct_from? is the one equality that may take nil.
98
+ def distinct_from?(value)
99
+ DistinctFrom.new(self, value, negated: true)
100
+ end
101
+
102
+ def not_distinct_from?(value)
103
+ DistinctFrom.new(self, value)
104
+ end
105
+
65
106
  def start_with?(*prefixes)
66
107
  if prefixes.empty?
67
108
  raise ArgumentError, "start_with? needs at least one prefix"
@@ -80,8 +121,75 @@ module ActiveRecord
80
121
  Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
81
122
  end
82
123
 
124
+ # The array comparisons carry the meaning of their Ruby namesakes.
125
+ # member? is Enumerable's element test, so an Array argument is
126
+ # rejected rather than quietly meaning something Array#member? does
127
+ # not; whole-array comparisons go by the Set and Array names.
83
128
  def member?(element)
84
- Member.new(self, element)
129
+ if element.is_a?(::Array) || element.is_a?(::Set)
130
+ raise ArgumentError,
131
+ "member? takes a single element; use superset? to require every element"
132
+ end
133
+ ArrayPredicate.new(self, :"@>", [element])
134
+ end
135
+
136
+ def superset?(elements)
137
+ ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
138
+ end
139
+
140
+ def subset?(elements)
141
+ ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
142
+ end
143
+
144
+ def intersect?(elements)
145
+ ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
146
+ end
147
+ end
148
+
149
+ # Arithmetic builders shared by symbols, qualified columns and
150
+ # expressions. Imported into the Symbol refinement like Predications,
151
+ # so every method must be defined with def.
152
+ module Arithmetics
153
+ def +(other)
154
+ Arithmetic.new(self, :+, other)
155
+ end
156
+
157
+ def -(other)
158
+ Arithmetic.new(self, :-, other)
159
+ end
160
+
161
+ def *(other)
162
+ Arithmetic.new(self, :*, other)
163
+ end
164
+
165
+ def /(other)
166
+ Arithmetic.new(self, :/, other)
167
+ end
168
+ end
169
+
170
+ # Aggregate builders shared by symbols, qualified columns and
171
+ # expressions. Imported into the Symbol refinement like Predications,
172
+ # so every method must be defined with def.
173
+ module Aggregations
174
+ # DISTINCT is Arel's only aggregate modifier, and only for count.
175
+ def count(distinct: false)
176
+ Aggregate.new(self, :count, distinct: distinct)
177
+ end
178
+
179
+ def sum
180
+ Aggregate.new(self, :sum)
181
+ end
182
+
183
+ def average
184
+ Aggregate.new(self, :average)
185
+ end
186
+
187
+ def maximum
188
+ Aggregate.new(self, :maximum)
189
+ end
190
+
191
+ def minimum
192
+ Aggregate.new(self, :minimum)
85
193
  end
86
194
  end
87
195
 
@@ -113,6 +221,15 @@ module ActiveRecord
113
221
  else operand
114
222
  end
115
223
  end
224
+
225
+ # Resolves a function argument: a column or an expression as above,
226
+ # anything else a value to be quoted.
227
+ def to_arel_argument(arg, table)
228
+ case arg
229
+ when Node, Symbol then to_arel_operand(arg, table)
230
+ else Arel::Nodes.build_quoted(arg)
231
+ end
232
+ end
116
233
  end
117
234
 
118
235
  class Predicate < Node
@@ -131,6 +248,8 @@ module ActiveRecord
131
248
 
132
249
  class Column < Node
133
250
  include Predications
251
+ include Arithmetics
252
+ include Aggregations
134
253
 
135
254
  attr_reader :table_name, :column_name
136
255
 
@@ -142,24 +261,55 @@ module ActiveRecord
142
261
  def to_arel(_table)
143
262
  Arel::Table.new(table_name)[column_name]
144
263
  end
264
+ end
265
+
266
+ # Arithmetic on columns and expressions. Ruby's precedence puts these
267
+ # above the comparison operators, so :price * :quantity > 100 groups the
268
+ # way it reads.
269
+ class Arithmetic < Node
270
+ include Predications
271
+ include Arithmetics
272
+ include Aggregations
273
+
274
+ attr_reader :left, :operator, :right
145
275
 
146
- %i[count sum average maximum minimum].each do |func|
147
- define_method(func) { Aggregate.new(self, func) }
276
+ def initialize(left, operator, right)
277
+ @left = left
278
+ @operator = operator
279
+ @right = right
280
+ end
281
+
282
+ def to_arel(table)
283
+ to_arel_operand(left, table).
284
+ public_send(operator, to_arel_operand(right, table))
148
285
  end
149
286
  end
150
287
 
151
288
  class Aggregate < Node
152
289
  include Predications
290
+ include Arithmetics
153
291
 
154
- attr_reader :operand, :function
292
+ attr_reader :operand, :function, :distinct
155
293
 
156
- def initialize(operand, function)
294
+ def initialize(operand, function, distinct: false)
295
+ if distinct && function != :count
296
+ raise ArgumentError, "#{function} does not take distinct"
297
+ end
298
+ if distinct && operand == :*
299
+ raise ArgumentError, "count(:*) does not take distinct; name a column"
300
+ end
157
301
  @operand = operand
158
302
  @function = function
303
+ @distinct = distinct
159
304
  end
160
305
 
161
306
  def to_arel(table)
162
- to_arel_operand(operand, table).public_send(function)
307
+ arel_operand = to_arel_operand(operand, table)
308
+ if function == :count
309
+ arel_operand.count(distinct)
310
+ else
311
+ arel_operand.public_send(function)
312
+ end
163
313
  end
164
314
  end
165
315
 
@@ -168,7 +318,7 @@ module ActiveRecord
168
318
 
169
319
  def initialize(operand, alias_name)
170
320
  @operand = operand
171
- @alias_name = alias_name
321
+ @alias_name = AST.check_name(alias_name, ALIAS_NAME, "column alias")
172
322
  end
173
323
 
174
324
  def to_arel(table)
@@ -177,20 +327,33 @@ module ActiveRecord
177
327
  end
178
328
 
179
329
  class Ordering < Node
180
- attr_reader :operand, :direction
330
+ attr_reader :operand, :direction, :nulls
181
331
 
182
- def initialize(operand, direction)
332
+ def initialize(operand, direction, nulls = nil)
183
333
  @operand = operand
184
334
  @direction = direction
335
+ @nulls = nulls
336
+ end
337
+
338
+ # MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
339
+ # leading IS NULL ordering, so these are portable.
340
+ def nulls_first
341
+ Ordering.new(operand, direction, :nulls_first)
342
+ end
343
+
344
+ def nulls_last
345
+ Ordering.new(operand, direction, :nulls_last)
185
346
  end
186
347
 
187
348
  def to_arel(table)
188
- to_arel_operand(operand, table).public_send(direction)
349
+ ordering = to_arel_operand(operand, table).public_send(direction)
350
+ nulls ? ordering.public_send(nulls) : ordering
189
351
  end
190
352
  end
191
353
 
192
354
  class Function < Node
193
355
  include Predications
356
+ include Arithmetics
194
357
 
195
358
  attr_reader :name, :args
196
359
 
@@ -200,16 +363,80 @@ module ActiveRecord
200
363
  end
201
364
 
202
365
  def to_arel(table)
203
- arel_args = args.map do |arg|
204
- case arg
205
- when Node, Symbol then to_arel_operand(arg, table)
206
- else Arel::Nodes.build_quoted(arg)
207
- end
208
- end
366
+ arel_args = args.map {|arg| to_arel_argument(arg, table) }
209
367
  Arel::Nodes::NamedFunction.new(name, arel_args)
210
368
  end
211
369
  end
212
370
 
371
+ # EXTRACT(field FROM expr). The field is grammar rather than a value --
372
+ # a keyword the adapter reads bare -- so it has to be a plain name,
373
+ # which Arel upcases on the way out.
374
+ class Extract < Node
375
+ include Predications
376
+ include Arithmetics
377
+
378
+ attr_reader :field, :operand
379
+
380
+ def initialize(field, operand)
381
+ @field = AST.check_name(field, ALIAS_NAME, "extract field")
382
+ @operand = operand
383
+ end
384
+
385
+ def to_arel(table)
386
+ Arel::Nodes::Extract.new(to_arel_argument(operand, table), field.to_s)
387
+ end
388
+ end
389
+
390
+ # CAST(expr AS type). The type is grammar too, written into the SQL as
391
+ # given -- it is the adapter's own name for the type, and whether it
392
+ # exists is the database's to say -- so it has to look like one:
393
+ # a plain name, at most parenthesized with lengths.
394
+ class Cast < Node
395
+ include Predications
396
+ include Arithmetics
397
+
398
+ attr_reader :operand, :sql_type
399
+
400
+ def initialize(operand, sql_type)
401
+ @operand = operand
402
+ @sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
403
+ end
404
+
405
+ def to_arel(table)
406
+ Arel::Nodes::NamedFunction.new(
407
+ "CAST",
408
+ [Arel::Nodes::As.new(to_arel_argument(operand, table),
409
+ Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
410
+ end
411
+ end
412
+
413
+ # CURRENT_TIMESTAMP and its relatives, what the SQL grammar calls a
414
+ # datetime value function. The grammar has them bare, and PostgreSQL
415
+ # and SQLite reject them written as calls, so unlike Function the name
416
+ # is emitted without parentheses. A precision is the one thing that
417
+ # does go into parentheses, and it is written into the SQL as given, so
418
+ # only an Integer is accepted.
419
+ class DatetimeValueFunction < Node
420
+ include Predications
421
+ include Arithmetics
422
+
423
+ attr_reader :name, :precision
424
+
425
+ def initialize(name, precision = nil)
426
+ unless precision.nil? || precision.is_a?(Integer)
427
+ raise ArgumentError,
428
+ "#{precision.inspect} is not an Integer precision"
429
+ end
430
+ @name = name
431
+ @precision = precision
432
+ end
433
+
434
+ def to_arel(_table)
435
+ Arel::Nodes::SqlLiteral.new(
436
+ precision ? "#{name}(#{precision})" : name)
437
+ end
438
+ end
439
+
213
440
  # A plain SQL comparison. The value is passed through as it is, so a Range
214
441
  # or an Array compares against a PostgreSQL range or array column, the way
215
442
  # ActiveRecord's own force_equality? types do.
@@ -229,9 +456,27 @@ module ActiveRecord
229
456
 
230
457
  def to_arel(table)
231
458
  arel_column = to_arel_operand(column, table)
232
- arel_value = value.is_a?(Node) ? value.to_arel(table) : value
459
+ arel_value =
460
+ case value
461
+ when Node then value.to_arel(table)
462
+ when ActiveRecord::Relation then scalar_subquery(value)
463
+ else value
464
+ end
233
465
  arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
234
466
  end
467
+
468
+ private
469
+
470
+ # A relation compared against a column has to yield a single value, so
471
+ # unlike In there is no sensible default select list to fall back on.
472
+ def scalar_subquery(relation)
473
+ if relation.select_values.empty?
474
+ raise ArgumentError,
475
+ "#{operator} needs a subquery selecting one value; add a select"
476
+ end
477
+ relation = relation.send(:apply_join_dependency) if relation.eager_loading?
478
+ relation.arel
479
+ end
235
480
  end
236
481
 
237
482
  # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
@@ -310,38 +555,80 @@ module ActiveRecord
310
555
  inject {|left, right| Or.new(left, right) }
311
556
  end
312
557
 
313
- attr_reader :operand, :pattern, :escape
558
+ attr_reader :operand, :pattern, :escape, :case_sensitive
314
559
 
315
- def initialize(operand, pattern, escape = nil)
560
+ def initialize(operand, pattern, escape = nil, case_sensitive: true)
316
561
  @operand = operand
317
562
  @pattern = pattern
318
563
  @escape = escape
564
+ @case_sensitive = case_sensitive
319
565
  end
320
566
 
321
567
  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)
568
+ # Arel matches case-insensitively unless told otherwise, which is
569
+ # what picks ILIKE over LIKE on PostgreSQL.
570
+ to_arel_operand(operand, table).matches(pattern, escape, case_sensitive)
325
571
  end
326
572
  end
327
573
 
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
574
+ # IS [NOT] DISTINCT FROM, spelled IS / IS NOT on SQLite and <=> on
575
+ # MySQL. NULL compares as a value here, which is what separates these
576
+ # from = and <>.
577
+ class DistinctFrom < Predicate
578
+ attr_reader :operand, :value, :negated
336
579
 
337
- def initialize(operand, element)
580
+ def initialize(operand, value, negated: false)
338
581
  @operand = operand
339
- @elements = element.is_a?(::Array) ? element : [element]
582
+ @value = value
583
+ @negated = negated
340
584
  end
341
585
 
342
586
  def to_arel(table)
343
587
  arel_operand = to_arel_operand(operand, table)
344
- arel_operand.contains(Arel::Nodes.build_quoted(array_literal))
588
+ arel_value = value.is_a?(Node) ? value.to_arel(table) : value
589
+ if negated
590
+ arel_operand.is_distinct_from(arel_value)
591
+ else
592
+ arel_operand.is_not_distinct_from(arel_value)
593
+ end
594
+ end
595
+ end
596
+
597
+ # Comparisons against a PostgreSQL array column, named after the Ruby
598
+ # methods that mean the same thing: member? is Enumerable's element
599
+ # test, superset? and subset? are Set's whole-array containment, and
600
+ # intersect? is Array's "any element in common". Each name maps to one
601
+ # operator; the elements are rendered as an array literal, which
602
+ # PostgreSQL coerces to the column's element type, so any expression
603
+ # works as the operand and no schema lookup is needed.
604
+ class ArrayPredicate < Predicate
605
+ # The whole-array comparisons take the collection kinds their
606
+ # namesakes compare against: an Array, or a Set for the Set methods.
607
+ def self.elements(arg, method_name)
608
+ case arg
609
+ when ::Array then arg
610
+ when ::Set then arg.to_a
611
+ else
612
+ raise ArgumentError, "#{method_name} takes an Array or Set of elements"
613
+ end
614
+ end
615
+
616
+ attr_reader :operand, :operator, :elements
617
+
618
+ def initialize(operand, operator, elements)
619
+ @operand = operand
620
+ @operator = operator
621
+ @elements = elements
622
+ end
623
+
624
+ def to_arel(table)
625
+ arel_operand = to_arel_operand(operand, table)
626
+ quoted = Arel::Nodes.build_quoted(array_literal)
627
+ case operator
628
+ when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
629
+ when :"&&" then Arel::Nodes::Overlaps.new(arel_operand, quoted)
630
+ else Arel::Nodes::InfixOperation.new(operator, arel_operand, quoted)
631
+ end
345
632
  end
346
633
 
347
634
  private