activerecord-refined 0.5.1 → 0.6.1

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,11 +1,9 @@
1
+ # JSON.generate, for the document a containment test is given.
2
+ require 'json'
3
+
1
4
  module ActiveRecord
2
5
  module Refined
3
6
  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
7
  NAME = /[[:alpha:]_][[:alnum:]_$]*/
10
8
  ALIAS_NAME = /\A#{NAME}\z/
11
9
  FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
@@ -13,6 +11,26 @@ module ActiveRecord
13
11
  # lengths -- double precision, decimal(10,2).
14
12
  TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
15
13
 
14
+ # Which family of spellings an adapter belongs to. MariaDB answers to
15
+ # the mysql2 adapter and is counted with MySQL, though the two part
16
+ # company over JSON. An adapter nobody has classified keeps the
17
+ # standard spellings and is left to say for itself what it cannot do.
18
+ #
19
+ # pglite is PostgreSQL itself compiled to WebAssembly, reached through
20
+ # wasmify-rails' adapter; the server it answers for is the same one.
21
+ ADAPTER_FAMILIES = {
22
+ 'sqlite3' => :sqlite,
23
+ 'postgresql' => :postgresql,
24
+ 'postgis' => :postgresql,
25
+ 'pglite' => :postgresql,
26
+ 'mysql2' => :mysql,
27
+ 'trilogy' => :mysql,
28
+ }.freeze
29
+
30
+ def self.adapter_family(model)
31
+ ADAPTER_FAMILIES[model.connection_db_config.adapter] || :unknown
32
+ end
33
+
16
34
  def self.check_name(name, pattern, what)
17
35
  return name if pattern.match?(name.to_s)
18
36
  raise ArgumentError, "#{name.inspect} is not a plain #{what}"
@@ -75,6 +93,27 @@ module ActiveRecord
75
93
  Comparison.new(self, :!=, nil)
76
94
  end
77
95
 
96
+ # IS TRUE and IS FALSE differ from a comparison against the literal in
97
+ # what they make of NULL: `flag = TRUE` is itself NULL there, and a
98
+ # NULL predicate selects nothing, while these two answer false. So the
99
+ # difference shows in the negations: `not_true?` keeps the NULL rows
100
+ # that `!(:flag == true)` drops.
101
+ def true?
102
+ TruthValue.new(self, true)
103
+ end
104
+
105
+ def not_true?
106
+ TruthValue.new(self, true, negated: true)
107
+ end
108
+
109
+ def false?
110
+ TruthValue.new(self, false)
111
+ end
112
+
113
+ def not_false?
114
+ TruthValue.new(self, false, negated: true)
115
+ end
116
+
78
117
  def in?(values)
79
118
  In.new(self, values)
80
119
  end
@@ -91,6 +130,13 @@ module ActiveRecord
91
130
  In.new(self, min..max, negated: true)
92
131
  end
93
132
 
133
+ # CASE with this as the operand, compared against each `when`:
134
+ # `:age.when(10).then(1).else(0)`. The other shape, where each `when`
135
+ # carries its own condition, starts at `case_when`.
136
+ def when(value = nil, &block)
137
+ Case.new(self).when(value, &block)
138
+ end
139
+
94
140
  def like?(pattern)
95
141
  Like.new(self, pattern)
96
142
  end
@@ -168,6 +214,39 @@ module ActiveRecord
168
214
  def intersect?(elements)
169
215
  ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
170
216
  end
217
+
218
+ # Reading inside a JSON document, by the name of what Hash does. A
219
+ # string or symbol steps into an object, an integer into an array, and
220
+ # what comes back is the value rather than the JSON, since that is what
221
+ # a comparison wants. dig_json keeps it JSON, for a document to be dug
222
+ # into further or compared whole.
223
+ def dig(*path)
224
+ JsonPath.new(self, path)
225
+ end
226
+
227
+ def dig_json(*path)
228
+ JsonPath.new(self, path, as_json: true)
229
+ end
230
+
231
+ # What dig reads, bury sets: the last argument is the value and the
232
+ # rest are the path to it. The document comes back changed rather
233
+ # than being written anywhere, which update_all is for.
234
+ def bury(*path, value)
235
+ JsonSet.new(self, path, value)
236
+ end
237
+
238
+ # Whether the document holds what is given, which SQL calls
239
+ # containment. SQLite has no equivalent.
240
+ def contains?(value)
241
+ JsonContains.new(self, value)
242
+ end
243
+
244
+ # Whether the key is there at all, as Hash#key? asks. Hash has
245
+ # has_key? too; one name is enough, and this is the one Ruby's own
246
+ # style prefers.
247
+ def key?(key)
248
+ JsonHasKey.new(self, key)
249
+ end
171
250
  end
172
251
 
173
252
  # Arithmetic builders shared by symbols, qualified columns and
@@ -189,41 +268,48 @@ module ActiveRecord
189
268
  def /(other)
190
269
  Arithmetic.new(self, :/, other)
191
270
  end
192
- end
193
271
 
194
- # Aggregate builders shared by symbols, qualified columns and
195
- # expressions. Imported into the Symbol refinement like Predications,
196
- # so every method must be defined with def.
197
- module Aggregations
198
- # DISTINCT is Arel's only aggregate modifier, and only for count.
199
- def count(distinct: false)
200
- Aggregate.new(self, :count, distinct: distinct)
272
+ # SQL's bitwise operators. & and | are AND and OR between conditions
273
+ # and are defined there, which is what leaves them free to mean here
274
+ # what SQL means by them. Ruby's precedence puts all six above the
275
+ # comparisons, so `:flags & 4 > 0` groups the way it reads.
276
+ def &(other)
277
+ Bitwise.new(self, :&, other)
278
+ end
279
+
280
+ def |(other)
281
+ Bitwise.new(self, :|, other)
201
282
  end
202
283
 
203
- def sum
204
- Aggregate.new(self, :sum)
284
+ def ^(other)
285
+ Bitwise.new(self, :^, other)
205
286
  end
206
287
 
207
- def average
208
- Aggregate.new(self, :average)
288
+ def <<(other)
289
+ Bitwise.new(self, :<<, other)
209
290
  end
210
291
 
211
- def maximum
212
- Aggregate.new(self, :maximum)
292
+ def >>(other)
293
+ Bitwise.new(self, :>>, other)
213
294
  end
214
295
 
215
- def minimum
216
- Aggregate.new(self, :minimum)
296
+ def ~
297
+ BitwiseNot.new(self)
217
298
  end
218
299
  end
219
300
 
220
301
  class Node
221
- def to_arel(table)
302
+ # The model travels with the table because some SQL cannot be written
303
+ # without knowing the adapter, and a node is built before anything
304
+ # knows which one it will be rendered for -- a symbol becomes a node
305
+ # inside a refinement, where there is no model to ask. Most nodes
306
+ # never look at it and only pass it on.
307
+ def to_arel(table, model)
222
308
  raise ScriptError, "subclass must override this method"
223
309
  end
224
310
 
225
- def as(alias_name)
226
- As.new(self, alias_name)
311
+ def as(alias_name, quote: true)
312
+ As.new(self, alias_name, quote: quote)
227
313
  end
228
314
 
229
315
  def asc
@@ -237,9 +323,9 @@ module ActiveRecord
237
323
  private
238
324
 
239
325
  # Resolves an operand denoting a column or an expression.
240
- def to_arel_operand(operand, table)
326
+ def to_arel_operand(operand, table, model)
241
327
  case operand
242
- when Node then operand.to_arel(table)
328
+ when Node then operand.to_arel(table, model)
243
329
  when :* then Arel.star
244
330
  when Symbol then table[operand]
245
331
  else operand
@@ -248,9 +334,9 @@ module ActiveRecord
248
334
 
249
335
  # Resolves a function argument: a column or an expression as above,
250
336
  # anything else a value to be quoted.
251
- def to_arel_argument(arg, table)
337
+ def to_arel_argument(arg, table, model)
252
338
  case arg
253
- when Node, Symbol then to_arel_operand(arg, table)
339
+ when Node, Symbol then to_arel_operand(arg, table, model)
254
340
  else Arel::Nodes.build_quoted(arg)
255
341
  end
256
342
  end
@@ -280,7 +366,6 @@ module ActiveRecord
280
366
  class Value < Node
281
367
  include Predications
282
368
  include Arithmetics
283
- include Aggregations
284
369
 
285
370
  attr_reader :value
286
371
 
@@ -288,15 +373,316 @@ module ActiveRecord
288
373
  @value = value
289
374
  end
290
375
 
291
- def to_arel(_table)
376
+ def to_arel(_table, _model)
292
377
  Arel::Nodes.build_quoted(value)
293
378
  end
294
379
  end
295
380
 
381
+ # CASE, in both of the shapes SQL has for it. With an operand, each
382
+ # `when` is something to compare it against; without one, each `when` is
383
+ # a condition of its own.
384
+ #
385
+ # Every method returns a new node rather than adding to this one, so a
386
+ # case kept in a variable can be branched from more than once.
387
+ class Case < Node
388
+ include Predications
389
+ include Arithmetics
390
+
391
+ # Having no ELSE is not the same as an ELSE of nil, and nil is what an
392
+ # omitted argument looks like, so the absence needs a value of its own.
393
+ NOTHING = Object.new.freeze
394
+ private_constant :NOTHING
395
+
396
+ attr_reader :operand, :whens, :default
397
+
398
+ def initialize(operand = nil, whens = [], default = NOTHING)
399
+ @operand = operand
400
+ @whens = whens
401
+ @default = default
402
+ end
403
+
404
+ def when(value = nil, &block)
405
+ Pending.new(self, Case.argument(:when, value, block))
406
+ end
407
+
408
+ # Kernel#then is on every object, so `then` in the wrong place would be
409
+ # answered by it -- with no block, silently, with an Enumerator.
410
+ def then(*)
411
+ raise ArgumentError, "then follows a when, and there is none to follow here"
412
+ end
413
+
414
+ def else(value = nil, &block)
415
+ Case.new(operand, whens, Case.argument(:else, value, block))
416
+ end
417
+
418
+ def to_arel(table, model)
419
+ raise ArgumentError, "case needs a when before it means anything" if whens.empty?
420
+
421
+ node = operand ? Arel::Nodes::Case.new(to_arel_operand(operand, table, model))
422
+ : Arel::Nodes::Case.new
423
+ whens.each do |condition, result|
424
+ node.when(to_arel_argument(condition, table, model)).
425
+ then(to_arel_argument(result, table, model))
426
+ end
427
+ node.else(to_arel_argument(default, table, model)) unless default.equal?(NOTHING)
428
+ node
429
+ end
430
+
431
+ # A value or a block, and exactly one of them: the block is what makes
432
+ # `when { :age >= 60 }` read like the blocks around it, and the value is
433
+ # what makes `when(10)` possible at all.
434
+ def self.argument(name, value, block)
435
+ if block
436
+ raise ArgumentError, "#{name} takes a value or a block, not both" unless value.nil?
437
+ return block.call
438
+ end
439
+ raise ArgumentError, "#{name} needs a value or a block" if value.nil?
440
+ value
441
+ end
442
+
443
+ # What a `when` is until its `then` arrives. A Node so that using it
444
+ # as one says what is missing rather than reaching ActiveRecord as
445
+ # something it cannot read.
446
+ class Pending < Node
447
+ def initialize(kase, condition)
448
+ @kase = kase
449
+ @condition = condition
450
+ end
451
+
452
+ def then(value = nil, &block)
453
+ Case.new(@kase.operand,
454
+ @kase.whens + [[@condition, Case.argument(:then, value, block)]],
455
+ @kase.default)
456
+ end
457
+
458
+ def to_arel(_table, _model)
459
+ raise ArgumentError, "when needs a matching then"
460
+ end
461
+ end
462
+ end
463
+
464
+ # A path into a JSON document, spelled the two ways the adapters want it.
465
+ # Shared, because reading a value and setting one walk the same path.
466
+ module JsonSteps
467
+ def check_steps(path, called)
468
+ raise ArgumentError, "#{called} needs a key or an index" if path.empty?
469
+ path.each do |step|
470
+ next if step.is_a?(::Integer) || step.is_a?(::String) || step.is_a?(::Symbol)
471
+ raise ArgumentError, "a step is a key or an array index, not #{step.inspect}"
472
+ end
473
+ path
474
+ end
475
+
476
+ # PostgreSQL takes the steps as a text array, where every element is
477
+ # quoted so that a comma or a brace in a key is part of it.
478
+ def steps_array
479
+ "{#{path.map {|step| %("#{escape_step(step)}") }.join(',')}}"
480
+ end
481
+
482
+ # MySQL and SQLite take a path expression instead, where an integer is
483
+ # a subscript and a name that is not plain has to be quoted.
484
+ def dollar_path
485
+ path.inject(+'$') do |so_far, step|
486
+ next so_far << "[#{step}]" if step.is_a?(::Integer)
487
+ name = step.to_s
488
+ so_far << '.' << (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
489
+ name : %("#{escape_step(step)}"))
490
+ end
491
+ end
492
+
493
+ def escape_step(step)
494
+ step.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
495
+ end
496
+ end
497
+
498
+ # Reading inside a JSON document. Every adapter can do it and no two
499
+ # spell it alike: PostgreSQL walks an array of steps, SQLite has the
500
+ # operators with a $ path, and MySQL has the functions -- which is what
501
+ # this uses for that family, since MariaDB answers to the same adapter
502
+ # and has no -> at all.
503
+ #
504
+ # The path is turned into a string either way, so a key with a space or
505
+ # a quote in it travels as itself rather than having to be refused.
506
+ class JsonPath < Node
507
+ include Predications
508
+ include Arithmetics
509
+ include JsonSteps
510
+
511
+ attr_reader :operand, :path, :as_json
512
+
513
+ def initialize(operand, path, as_json: false)
514
+ @operand = operand
515
+ @path = check_steps(path, 'dig')
516
+ @as_json = as_json
517
+ end
518
+
519
+ def to_arel(table, model)
520
+ document = to_arel_operand(operand, table, model)
521
+ case AST.adapter_family(model)
522
+ when :postgresql
523
+ Arel::Nodes::InfixOperation.new(
524
+ as_json ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps_array))
525
+ when :mysql
526
+ extracted = Arel::Nodes::NamedFunction.new(
527
+ 'JSON_EXTRACT', [document, Arel::Nodes.build_quoted(dollar_path)])
528
+ as_json ? extracted : Arel::Nodes::NamedFunction.new('JSON_UNQUOTE', [extracted])
529
+ else
530
+ extracted = Arel::Nodes::InfixOperation.new(
531
+ as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
532
+ # SQLite's ->> gives back the value with its type, where the other
533
+ # two give text. Cast so that `dig(:n) == '5'` means the same
534
+ # thing everywhere, and a number wants a cast everywhere too.
535
+ as_json ? extracted : Arel::Nodes::NamedFunction.new(
536
+ 'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
537
+ end
538
+ end
539
+
540
+ end
541
+
542
+ # Setting a value inside a JSON document, which is what bury does to what
543
+ # dig reads. The document comes back changed rather than being written
544
+ # anywhere; update_all is what writes it.
545
+ class JsonSet < Node
546
+ include Predications
547
+ include JsonSteps
548
+
549
+ attr_reader :operand, :path, :value
550
+
551
+ def initialize(operand, path, value)
552
+ @operand = operand
553
+ @path = check_steps(path, 'bury')
554
+ @value = value
555
+ end
556
+
557
+ def to_arel(table, model)
558
+ document = to_arel_operand(operand, table, model)
559
+ if AST.adapter_family(model) == :postgresql
560
+ Arel::Nodes::NamedFunction.new(
561
+ 'jsonb_set',
562
+ [document, Arel::Nodes.build_quoted(steps_array), postgresql_value(table, model)])
563
+ else
564
+ Arel::Nodes::NamedFunction.new(
565
+ 'JSON_SET',
566
+ [document, Arel::Nodes.build_quoted(dollar_path), other_value(table, model)])
567
+ end
568
+ end
569
+
570
+ private
571
+
572
+ # jsonb_set takes jsonb, so an expression is turned into it and a Ruby
573
+ # value goes in as the JSON that says it -- '"x"' rather than 'x',
574
+ # which is not a document at all.
575
+ def postgresql_value(table, model)
576
+ return Arel::Nodes::NamedFunction.new(
577
+ 'to_jsonb', [to_arel_operand(value, table, model)]) if expression?
578
+ Arel::Nodes.build_quoted(JSON.generate(value))
579
+ end
580
+
581
+ # The others take the value as it is, except a whole document, which
582
+ # they read out of a literal rather than take as a string. MySQL casts
583
+ # to JSON where MariaDB, which answers to the same adapter, does not.
584
+ def other_value(table, model)
585
+ return to_arel_operand(value, table, model) if expression?
586
+ return Arel::Nodes.build_quoted(value) unless value.is_a?(::Hash) || value.is_a?(::Array)
587
+
588
+ Arel::Nodes::NamedFunction.new(
589
+ 'JSON_EXTRACT',
590
+ [Arel::Nodes.build_quoted(JSON.generate(value)), Arel::Nodes.build_quoted('$')])
591
+ end
592
+
593
+ def expression?
594
+ value.is_a?(Node) || value.is_a?(::Symbol)
595
+ end
596
+ end
597
+
598
+ # JSON containment: whether the document holds what is given.
599
+ class JsonContains < Predicate
600
+ attr_reader :operand, :value
601
+
602
+ def initialize(operand, value)
603
+ @operand = operand
604
+ @value = value
605
+ end
606
+
607
+ def to_arel(table, model)
608
+ document = to_arel_operand(operand, table, model)
609
+ json = Arel::Nodes.build_quoted(JSON.generate(value))
610
+ case AST.adapter_family(model)
611
+ when :postgresql then Arel::Nodes::Contains.new(document, json)
612
+ when :mysql
613
+ Arel::Nodes::NamedFunction.new('JSON_CONTAINS', [document, json])
614
+ else
615
+ # Later than the others, since the adapter is only known here.
616
+ raise NotImplementedError,
617
+ "contains? has no equivalent on #{model.connection_db_config.adapter}"
618
+ end
619
+ end
620
+ end
621
+
622
+ # Whether a key is in the document. PostgreSQL has an operator for it,
623
+ # ?, which is also what a bind parameter looks like to several drivers;
624
+ # the function it is shorthand for says the same thing and survives.
625
+ class JsonHasKey < Predicate
626
+ attr_reader :operand, :key
627
+
628
+ def initialize(operand, key)
629
+ @operand = operand
630
+ @key = key
631
+ end
632
+
633
+ def to_arel(table, model)
634
+ document = to_arel_operand(operand, table, model)
635
+ name = Arel::Nodes.build_quoted(key.to_s)
636
+ path = Arel::Nodes.build_quoted("$.#{key}")
637
+ case AST.adapter_family(model)
638
+ when :postgresql
639
+ Arel::Nodes::NamedFunction.new('jsonb_exists', [document, name])
640
+ when :mysql
641
+ Arel::Nodes::NamedFunction.new(
642
+ 'JSON_CONTAINS_PATH', [document, Arel::Nodes.build_quoted('one'), path])
643
+ else
644
+ Arel::Nodes::NamedFunction.new('json_type', [document, path]).not_eq(nil)
645
+ end
646
+ end
647
+ end
648
+
649
+ # GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
650
+ # once, the totals of each coming back beside the rows. PostgreSQL has
651
+ # all three; the block raises for the others before it gets this far.
652
+ #
653
+ # Each set is a list of its own, so grouping_sets takes lists and rollup
654
+ # and cube take the columns themselves.
655
+ class GroupingSets < Node
656
+ KINDS = {
657
+ grouping_sets: Arel::Nodes::GroupingSet,
658
+ rollup: Arel::Nodes::RollUp,
659
+ cube: Arel::Nodes::Cube,
660
+ }.freeze
661
+
662
+ attr_reader :kind, :sets
663
+
664
+ def initialize(kind, sets)
665
+ raise ArgumentError, "#{kind} needs something to group by" if sets.empty?
666
+ @kind = kind
667
+ @sets = sets
668
+ end
669
+
670
+ def to_arel(table, model)
671
+ KINDS.fetch(kind).new(
672
+ if kind == :grouping_sets
673
+ sets.map do |set|
674
+ Arel::Nodes::GroupingElement.new(
675
+ Array(set).map {|column| to_arel_operand(column, table, model) })
676
+ end
677
+ else
678
+ sets.map {|column| to_arel_operand(column, table, model) }
679
+ end)
680
+ end
681
+ end
682
+
296
683
  class Column < Node
297
684
  include Predications
298
685
  include Arithmetics
299
- include Aggregations
300
686
 
301
687
  attr_reader :table_name, :column_name
302
688
 
@@ -305,7 +691,7 @@ module ActiveRecord
305
691
  @column_name = column_name
306
692
  end
307
693
 
308
- def to_arel(_table)
694
+ def to_arel(_table, _model)
309
695
  Arel::Table.new(table_name)[column_name]
310
696
  end
311
697
  end
@@ -316,7 +702,6 @@ module ActiveRecord
316
702
  class Arithmetic < Node
317
703
  include Predications
318
704
  include Arithmetics
319
- include Aggregations
320
705
 
321
706
  attr_reader :left, :operator, :right
322
707
 
@@ -326,19 +711,218 @@ module ActiveRecord
326
711
  @right = right
327
712
  end
328
713
 
329
- def to_arel(table)
330
- to_arel_operand(left, table).
331
- public_send(operator, to_arel_operand(right, table))
714
+ def to_arel(table, model)
715
+ to_arel_operand(left, table, model).
716
+ public_send(operator, to_arel_operand(right, table, model))
717
+ end
718
+ end
719
+
720
+ # What the bitwise operators refuse. Both refusals are there because
721
+ # the same Ruby would otherwise mean different things per adapter: MySQL
722
+ # and SQLite take a boolean for the one bit it is stored as, so
723
+ # `published & active` would quietly be the AND it looks like, while
724
+ # PostgreSQL has no such operator and would say so.
725
+ module BitwiseOperands
726
+ private
727
+
728
+ def check_operand(operand, operator)
729
+ return operand unless operand.is_a?(Predicate)
730
+ raise ArgumentError,
731
+ "a condition cannot be an operand of #{operator}; " \
732
+ "& and | between conditions are AND and OR"
733
+ end
734
+
735
+ # Only the unqualified column can be checked, since that is the one
736
+ # the model is known to have.
737
+ def check_not_boolean(operand, operator, model)
738
+ return unless operand.is_a?(::Symbol)
739
+ return unless model.type_for_attribute(operand).type == :boolean
740
+ raise ArgumentError,
741
+ "#{operand.inspect} is a boolean column, which #{operator} does " \
742
+ "not take; #{operand.inspect}.true? is the condition"
743
+ end
744
+ end
745
+
746
+ # SQL's bitwise operators. Each parenthesises itself, which is what
747
+ # keeps Ruby's grouping: PostgreSQL gives & and | the same precedence
748
+ # and reads a | b & c from the left, where Ruby reads the & first.
749
+ class Bitwise < Node
750
+ include Predications
751
+ include Arithmetics
752
+ include BitwiseOperands
753
+
754
+ NODES = {
755
+ :& => Arel::Nodes::BitwiseAnd,
756
+ :| => Arel::Nodes::BitwiseOr,
757
+ :<< => Arel::Nodes::BitwiseShiftLeft,
758
+ :>> => Arel::Nodes::BitwiseShiftRight,
759
+ }.freeze
760
+
761
+ attr_reader :left, :operator, :right
762
+
763
+ def initialize(left, operator, right)
764
+ @left = left
765
+ @operator = operator
766
+ @right = check_operand(right, operator)
767
+ end
768
+
769
+ def to_arel(table, model)
770
+ check_not_boolean(left, operator, model)
771
+ check_not_boolean(right, operator, model)
772
+ arel_left = to_arel_operand(left, table, model)
773
+ arel_right = to_arel_argument(right, table, model)
774
+ Arel::Nodes::Grouping.new(
775
+ if operator == :^
776
+ xor(arel_left, arel_right, model)
777
+ else
778
+ NODES.fetch(operator).new(arel_left, arel_right)
779
+ end)
780
+ end
781
+
782
+ private
783
+
784
+ # Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
785
+ # exponentiation to PostgreSQL -- a wrong answer rather than an error.
786
+ # PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
787
+ # it cannot be the portable one either. SQLite has no XOR at all;
788
+ # (a | b) - (a & b) is it, at the cost of naming each operand twice.
789
+ def xor(left, right, model)
790
+ case AST.adapter_family(model)
791
+ when :postgresql then Arel::Nodes::InfixOperation.new('#', left, right)
792
+ when :mysql then Arel::Nodes::BitwiseXor.new(left, right)
793
+ else
794
+ Arel::Nodes::Subtraction.new(
795
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
796
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
797
+ end
798
+ end
799
+ end
800
+
801
+ # ~, which every adapter has. MySQL answers with the unsigned 64-bit
802
+ # number where the others answer with a negative one; the bits are the
803
+ # same, and only reading the value back tells them apart.
804
+ class BitwiseNot < Node
805
+ include Predications
806
+ include Arithmetics
807
+ include BitwiseOperands
808
+
809
+ attr_reader :operand
810
+
811
+ def initialize(operand)
812
+ @operand = check_operand(operand, :~)
813
+ end
814
+
815
+ def to_arel(table, model)
816
+ check_not_boolean(operand, :~, model)
817
+ Arel::Nodes::Grouping.new(
818
+ Arel::Nodes::BitwiseNot.new(to_arel_operand(operand, table, model)))
819
+ end
820
+ end
821
+
822
+ # OVER, on the two things that can carry a window: an aggregate, and a
823
+ # function.
824
+ module Windowing
825
+ def over
826
+ Over.new(self)
827
+ end
828
+ end
829
+
830
+ # A function with a window. The window is built by chaining, the way
831
+ # Arel's own is, and each method returns a new node rather than adding to
832
+ # this one, so a window can be finished more than one way.
833
+ class Over < Node
834
+ include Predications
835
+ include Arithmetics
836
+
837
+ attr_reader :function, :partitions, :orders, :frame
838
+
839
+ def initialize(function, partitions = [], orders = [], frame = nil)
840
+ @function = function
841
+ @partitions = partitions
842
+ @orders = orders
843
+ @frame = frame
844
+ end
845
+
846
+ def partition(*exprs)
847
+ raise ArgumentError, "partition needs an expression" if exprs.empty?
848
+ Over.new(function, partitions + exprs, orders, frame)
849
+ end
850
+
851
+ def order(*exprs)
852
+ raise ArgumentError, "order needs an expression" if exprs.empty?
853
+ Over.new(function, partitions, orders + exprs, frame)
854
+ end
855
+
856
+ def rows(bounds)
857
+ Over.new(function, partitions, orders, framing(:rows, bounds))
858
+ end
859
+
860
+ def range(bounds)
861
+ Over.new(function, partitions, orders, framing(:range, bounds))
862
+ end
863
+
864
+ def to_arel(table, model)
865
+ window = Arel::Nodes::Window.new
866
+ partitions.each {|expr| window.partition(to_arel_operand(expr, table, model)) }
867
+ orders.each {|expr| window.order(to_arel_operand(expr, table, model)) }
868
+ frame_arel(window) if frame
869
+
870
+ # A window-only function refuses to build on its own; here is where
871
+ # it is asked for the call itself.
872
+ arel_function =
873
+ function.is_a?(WindowFunction) ? function.call_arel(table, model)
874
+ : function.to_arel(table, model)
875
+ Arel::Nodes::Over.new(arel_function, window)
876
+ end
877
+
878
+ private
879
+
880
+ # The frame is a range of rows counted from the current one: negative
881
+ # before it, positive after, 0 the row itself, and an open end for
882
+ # unbounded. `rows(..0)` is what a running total wants.
883
+ def framing(kind, bounds)
884
+ raise ArgumentError, "a window has one frame" if frame
885
+ unless bounds.is_a?(::Range)
886
+ raise ArgumentError, "#{kind} takes a range of rows, as in rows(..0)"
887
+ end
888
+ if bounds.exclude_end?
889
+ raise ArgumentError, "a frame ends on a row rather than before one; use .."
890
+ end
891
+ [bounds.begin, bounds.end].each do |bound|
892
+ next if bound.nil? || bound.is_a?(::Integer)
893
+ raise ArgumentError,
894
+ "a frame bound is a number of rows, or nothing for unbounded"
895
+ end
896
+ [kind, bounds.begin, bounds.end]
897
+ end
898
+
899
+ # Arel wants the keyword itself on the left of the BETWEEN, which is
900
+ # what window.rows with no argument hands back.
901
+ def frame_arel(window)
902
+ kind, from, to = frame
903
+ window.frame(
904
+ Arel::Nodes::Between.new(
905
+ window.public_send(kind),
906
+ Arel::Nodes::And.new([bound(from, Arel::Nodes::Preceding.new),
907
+ bound(to, Arel::Nodes::Following.new)])))
908
+ end
909
+
910
+ def bound(rows, unbounded)
911
+ return unbounded if rows.nil?
912
+ return Arel::Nodes::CurrentRow.new if rows.zero?
913
+ rows.negative? ? Arel::Nodes::Preceding.new(-rows)
914
+ : Arel::Nodes::Following.new(rows)
332
915
  end
333
916
  end
334
917
 
335
918
  class Aggregate < Node
336
919
  include Predications
337
920
  include Arithmetics
921
+ include Windowing
338
922
 
339
- attr_reader :operand, :function, :distinct
923
+ attr_reader :operand, :function, :distinct, :condition
340
924
 
341
- def initialize(operand, function, distinct: false)
925
+ def initialize(operand, function, distinct: false, condition: nil)
342
926
  if distinct && function != :count
343
927
  raise ArgumentError, "#{function} does not take distinct"
344
928
  end
@@ -348,10 +932,35 @@ module ActiveRecord
348
932
  @operand = operand
349
933
  @function = function
350
934
  @distinct = distinct
935
+ @condition = condition
936
+ end
937
+
938
+ # FILTER (WHERE ...): the aggregate is taken over the rows the
939
+ # condition holds for. A value or a block, as `when` takes them.
940
+ def filter(condition = nil, &block)
941
+ Aggregate.new(operand, function, distinct: distinct,
942
+ condition: Case.argument(:filter, condition, block))
351
943
  end
352
944
 
353
- def to_arel(table)
354
- arel_operand = to_arel_operand(operand, table)
945
+ def to_arel(table, model)
946
+ return aggregate(operand, table, model) unless condition
947
+
948
+ # MySQL has no FILTER clause. An aggregate passes over a NULL, so
949
+ # the case that yields nothing for the rows the condition misses is
950
+ # the same aggregate over the same rows -- count(*) has no operand to
951
+ # keep, and counts a 1 instead.
952
+ if AST.adapter_family(model) == :mysql
953
+ kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
954
+ return aggregate(kept, table, model)
955
+ end
956
+
957
+ aggregate(operand, table, model).filter(condition.to_arel(table, model))
958
+ end
959
+
960
+ private
961
+
962
+ def aggregate(over, table, model)
963
+ arel_operand = to_arel_operand(over, table, model)
355
964
  if function == :count
356
965
  arel_operand.count(distinct)
357
966
  else
@@ -360,16 +969,37 @@ module ActiveRecord
360
969
  end
361
970
  end
362
971
 
972
+ # A column alias, quoted by the adapter, so that the name asked for is
973
+ # the name that comes back: unquoted, PostgreSQL folds a capital away
974
+ # and the other two keep it, which is one block meaning two things.
975
+ # Quoting also leaves nothing to refuse -- a name that would have been
976
+ # SQL is an identifier with a strange name instead.
977
+ #
978
+ # `quote: false` asks for the name as written, for a schema that wants
979
+ # the folding.
363
980
  class As < Node
364
- attr_reader :operand, :alias_name
981
+ attr_reader :operand, :alias_name, :quote
365
982
 
366
- def initialize(operand, alias_name)
983
+ def initialize(operand, alias_name, quote: true)
984
+ # Checked here rather than where the SQL is built, so that a name
985
+ # the adapter is not being asked to quote is refused where it was
986
+ # written.
987
+ AST.check_name(alias_name, ALIAS_NAME, "column alias") unless quote
367
988
  @operand = operand
368
- @alias_name = AST.check_name(alias_name, ALIAS_NAME, "column alias")
989
+ @alias_name = alias_name
990
+ @quote = quote
369
991
  end
370
992
 
371
- def to_arel(table)
372
- to_arel_operand(operand, table).as(alias_name.to_s)
993
+ def to_arel(table, model)
994
+ to_arel_operand(operand, table, model).as(alias_sql(model))
995
+ end
996
+
997
+ private
998
+
999
+ def alias_sql(model)
1000
+ name = alias_name.to_s
1001
+ return name unless quote
1002
+ model.with_connection {|connection| connection.quote_column_name(name) }
373
1003
  end
374
1004
  end
375
1005
 
@@ -392,8 +1022,8 @@ module ActiveRecord
392
1022
  Ordering.new(operand, direction, :nulls_last)
393
1023
  end
394
1024
 
395
- def to_arel(table)
396
- ordering = to_arel_operand(operand, table).public_send(direction)
1025
+ def to_arel(table, model)
1026
+ ordering = to_arel_operand(operand, table, model).public_send(direction)
397
1027
  nulls ? ordering.public_send(nulls) : ordering
398
1028
  end
399
1029
  end
@@ -401,6 +1031,7 @@ module ActiveRecord
401
1031
  class Function < Node
402
1032
  include Predications
403
1033
  include Arithmetics
1034
+ include Windowing
404
1035
 
405
1036
  attr_reader :name, :args
406
1037
 
@@ -409,12 +1040,23 @@ module ActiveRecord
409
1040
  @args = args
410
1041
  end
411
1042
 
412
- def to_arel(table)
413
- arel_args = args.map {|arg| to_arel_argument(arg, table) }
1043
+ def to_arel(table, model)
1044
+ arel_args = args.map {|arg| to_arel_argument(arg, table, model) }
414
1045
  Arel::Nodes::NamedFunction.new(name, arel_args)
415
1046
  end
416
1047
  end
417
1048
 
1049
+ # ROW_NUMBER and its kind: functions that say nothing without a window.
1050
+ # On its own this refuses rather than reaching the database as an error
1051
+ # there; over asks it for call_arel instead.
1052
+ class WindowFunction < Function
1053
+ alias_method :call_arel, :to_arel
1054
+
1055
+ def to_arel(_table, _model)
1056
+ raise ArgumentError, "#{name.downcase} is a window function; it needs over"
1057
+ end
1058
+ end
1059
+
418
1060
  # EXTRACT(field FROM expr). The field is grammar rather than a value --
419
1061
  # a keyword the adapter reads bare -- so it has to be a plain name,
420
1062
  # which Arel upcases on the way out.
@@ -429,8 +1071,8 @@ module ActiveRecord
429
1071
  @operand = operand
430
1072
  end
431
1073
 
432
- def to_arel(table)
433
- Arel::Nodes::Extract.new(to_arel_argument(operand, table), field.to_s)
1074
+ def to_arel(table, model)
1075
+ Arel::Nodes::Extract.new(to_arel_argument(operand, table, model), field.to_s)
434
1076
  end
435
1077
  end
436
1078
 
@@ -449,10 +1091,10 @@ module ActiveRecord
449
1091
  @sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
450
1092
  end
451
1093
 
452
- def to_arel(table)
1094
+ def to_arel(table, model)
453
1095
  Arel::Nodes::NamedFunction.new(
454
1096
  "CAST",
455
- [Arel::Nodes::As.new(to_arel_argument(operand, table),
1097
+ [Arel::Nodes::As.new(to_arel_argument(operand, table, model),
456
1098
  Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
457
1099
  end
458
1100
  end
@@ -478,7 +1120,7 @@ module ActiveRecord
478
1120
  @precision = precision
479
1121
  end
480
1122
 
481
- def to_arel(_table)
1123
+ def to_arel(_table, _model)
482
1124
  Arel::Nodes::SqlLiteral.new(
483
1125
  precision ? "#{name}(#{precision})" : name)
484
1126
  end
@@ -501,11 +1143,11 @@ module ActiveRecord
501
1143
  @value = value
502
1144
  end
503
1145
 
504
- def to_arel(table)
505
- arel_column = to_arel_operand(column, table)
1146
+ def to_arel(table, model)
1147
+ arel_column = to_arel_operand(column, table, model)
506
1148
  arel_value =
507
1149
  case value
508
- when Node then value.to_arel(table)
1150
+ when Node then value.to_arel(table, model)
509
1151
  when ActiveRecord::Relation then scalar_subquery(value)
510
1152
  else value
511
1153
  end
@@ -526,9 +1168,50 @@ module ActiveRecord
526
1168
  end
527
1169
  end
528
1170
 
1171
+ # IS TRUE, IS FALSE and their negations, which every adapter spells the
1172
+ # same way and answers alike, NULL included.
1173
+ class TruthValue < Predicate
1174
+ attr_reader :operand, :value, :negated
1175
+
1176
+ def initialize(operand, value, negated: false)
1177
+ @operand = operand
1178
+ @value = value
1179
+ @negated = negated
1180
+ end
1181
+
1182
+ def to_arel(table, model)
1183
+ literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
1184
+ Arel::Nodes::InfixOperation.new(negated ? 'IS NOT' : 'IS',
1185
+ to_arel_operand(operand, table, model), literal)
1186
+ end
1187
+ end
1188
+
1189
+ # A relation standing for a set of values, which is what IN and the
1190
+ # quantifiers each take. The treatment is ActiveRecord's own
1191
+ # RelationHandler's: without an explicit select list the subquery
1192
+ # selects the model's primary key.
1193
+ module SetSubquery
1194
+ private
1195
+
1196
+ def set_subquery(relation, spelling)
1197
+ relation = relation.send(:apply_join_dependency) if relation.eager_loading?
1198
+ if relation.select_values.empty?
1199
+ model = relation.model
1200
+ if model.composite_primary_key?
1201
+ raise ArgumentError,
1202
+ "Cannot map composite primary key #{model.primary_key} to #{spelling}"
1203
+ end
1204
+ relation = relation.select(relation.table[model.primary_key])
1205
+ end
1206
+ relation.arel
1207
+ end
1208
+ end
1209
+
529
1210
  # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
530
1211
  # relation.
531
1212
  class In < Predicate
1213
+ include SetSubquery
1214
+
532
1215
  attr_reader :operand, :values, :negated
533
1216
 
534
1217
  def initialize(operand, values, negated: false)
@@ -537,34 +1220,39 @@ module ActiveRecord
537
1220
  @negated = negated
538
1221
  end
539
1222
 
540
- def to_arel(table)
541
- arel_operand = to_arel_operand(operand, table)
1223
+ def to_arel(table, model)
1224
+ arel_operand = to_arel_operand(operand, table, model)
542
1225
  if values.is_a?(Range)
543
1226
  arel_operand.public_send(negated ? :not_between : :between, values)
544
1227
  else
545
- arg = values.is_a?(ActiveRecord::Relation) ? subquery(values) : values
1228
+ arg = values.is_a?(ActiveRecord::Relation) ? set_subquery(values, 'IN') : values
546
1229
  arel_operand.public_send(negated ? :not_in : :in, arg)
547
1230
  end
548
1231
  end
1232
+ end
549
1233
 
550
- private
1234
+ # ANY and ALL, which stand on the right of a comparison and say how many
1235
+ # of the subquery's rows have to satisfy it. Where a scalar subquery
1236
+ # has to return one row, these take as many as come.
1237
+ class Quantified < Node
1238
+ include SetSubquery
551
1239
 
552
- # The same treatment ActiveRecord's own RelationHandler gives a
553
- # relation used as a value: without an explicit select list the
554
- # subquery selects the model's primary key.
555
- def subquery(relation)
556
- if relation.eager_loading?
557
- relation = relation.send(:apply_join_dependency)
558
- end
559
- if relation.select_values.empty?
560
- model = relation.model
561
- if model.composite_primary_key?
562
- raise ArgumentError,
563
- "Cannot map composite primary key #{model.primary_key} to IN"
564
- end
565
- relation = relation.select(relation.table[model.primary_key])
1240
+ attr_reader :kind, :relation
1241
+
1242
+ def initialize(kind, relation)
1243
+ unless relation.is_a?(ActiveRecord::Relation)
1244
+ raise ArgumentError,
1245
+ "#{kind} takes a relation as its subquery; a list is what in? takes"
566
1246
  end
567
- relation.arel
1247
+ @kind = kind
1248
+ @relation = relation
1249
+ end
1250
+
1251
+ # The subquery goes in as its own AST rather than as the manager,
1252
+ # which would parenthesise it a second time -- and to PostgreSQL
1253
+ # `ANY ((SELECT ...))` is ANY of one scalar, which it refuses.
1254
+ def to_arel(_table, _model)
1255
+ Arel::Nodes::NamedFunction.new(kind, [set_subquery(relation, kind).ast])
568
1256
  end
569
1257
  end
570
1258
 
@@ -578,7 +1266,7 @@ module ActiveRecord
578
1266
  @relation = relation
579
1267
  end
580
1268
 
581
- def to_arel(_table)
1269
+ def to_arel(_table, _model)
582
1270
  subquery = relation
583
1271
  if subquery.eager_loading?
584
1272
  subquery = subquery.send(:apply_join_dependency)
@@ -615,10 +1303,10 @@ module ActiveRecord
615
1303
  @negated = negated
616
1304
  end
617
1305
 
618
- def to_arel(table)
1306
+ def to_arel(table, model)
619
1307
  # Arel matches case-insensitively unless told otherwise, which is
620
1308
  # what picks ILIKE over LIKE on PostgreSQL.
621
- to_arel_operand(operand, table).
1309
+ to_arel_operand(operand, table, model).
622
1310
  public_send(negated ? :does_not_match : :matches,
623
1311
  pattern, escape, case_sensitive)
624
1312
  end
@@ -636,9 +1324,9 @@ module ActiveRecord
636
1324
  @negated = negated
637
1325
  end
638
1326
 
639
- def to_arel(table)
640
- arel_operand = to_arel_operand(operand, table)
641
- arel_value = value.is_a?(Node) ? value.to_arel(table) : value
1327
+ def to_arel(table, model)
1328
+ arel_operand = to_arel_operand(operand, table, model)
1329
+ arel_value = value.is_a?(Node) ? value.to_arel(table, model) : value
642
1330
  if negated
643
1331
  arel_operand.is_distinct_from(arel_value)
644
1332
  else
@@ -674,8 +1362,8 @@ module ActiveRecord
674
1362
  @elements = elements
675
1363
  end
676
1364
 
677
- def to_arel(table)
678
- arel_operand = to_arel_operand(operand, table)
1365
+ def to_arel(table, model)
1366
+ arel_operand = to_arel_operand(operand, table, model)
679
1367
  quoted = Arel::Nodes.build_quoted(array_literal)
680
1368
  case operator
681
1369
  when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
@@ -713,8 +1401,8 @@ module ActiveRecord
713
1401
  @negated = negated
714
1402
  end
715
1403
 
716
- def to_arel(table)
717
- arel_operand = to_arel_operand(operand, table)
1404
+ def to_arel(table, model)
1405
+ arel_operand = to_arel_operand(operand, table, model)
718
1406
  if negated
719
1407
  arel_operand.does_not_match_regexp(pattern)
720
1408
  else
@@ -746,8 +1434,8 @@ module ActiveRecord
746
1434
  @right = right
747
1435
  end
748
1436
 
749
- def to_arel(table)
750
- left.to_arel(table).and(right.to_arel(table))
1437
+ def to_arel(table, model)
1438
+ left.to_arel(table, model).and(right.to_arel(table, model))
751
1439
  end
752
1440
  end
753
1441
 
@@ -759,8 +1447,8 @@ module ActiveRecord
759
1447
  @right = right
760
1448
  end
761
1449
 
762
- def to_arel(table)
763
- left.to_arel(table).or(right.to_arel(table))
1450
+ def to_arel(table, model)
1451
+ left.to_arel(table, model).or(right.to_arel(table, model))
764
1452
  end
765
1453
  end
766
1454
 
@@ -771,8 +1459,8 @@ module ActiveRecord
771
1459
  @operand = operand
772
1460
  end
773
1461
 
774
- def to_arel(table)
775
- Arel::Nodes::Not.new(operand.to_arel(table))
1462
+ def to_arel(table, model)
1463
+ Arel::Nodes::Not.new(operand.to_arel(table, model))
776
1464
  end
777
1465
  end
778
1466
  end