activerecord-refined 0.8.0 → 0.9.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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module Refined
3
5
  module BlockSyntax
@@ -22,17 +24,26 @@ module ActiveRecord
22
24
  end
23
25
  end
24
26
 
25
- # Shorthand for `value(0).as(:depth)` and the like. Numbers only: a
26
- # string in a select list already means SQL rather than a string, so
27
- # giving String this would make the same literal mean two things
28
- # depending on whether it had been sent a message.
29
- [Integer, Float].each do |klass|
27
+ # Shorthand for `value(0).as(:depth)` and the like, and arithmetic with
28
+ # the number on the left: 20 - :quantity. BigDecimal is a number here
29
+ # because that is what a decimal column's values are.
30
+ [Integer, Float, BigDecimal].each do |klass|
30
31
  refine klass do
32
+ import_methods AST::NumericArithmetics
33
+
31
34
  def as(alias_name, quote: true)
32
35
  AST::As.new(AST::Value.new(self), alias_name, quote: quote)
33
36
  end
34
37
  end
35
38
  end
39
+
40
+ # A string is a value here as it is in every other position of a block;
41
+ # SQL is asked for by name, with sql().
42
+ refine String do
43
+ def as(alias_name, quote: true)
44
+ AST::As.new(AST::Value.new(self), alias_name, quote: quote)
45
+ end
46
+ end
36
47
  end
37
48
 
38
49
  class BlockContext
@@ -47,13 +58,34 @@ module ActiveRecord
47
58
  }.freeze
48
59
 
49
60
  AGGREGATE_FUNCTIONS.each do |name, arel_func|
50
- define_method(name) {|column| AST::Aggregate.new(column, arel_func) }
61
+ define_method(name) { |column| AST::Aggregate.new(column, arel_func) }
51
62
  end
52
63
 
53
64
  def count(column, distinct: false)
54
65
  AST::Aggregate.new(column, :count, distinct: distinct)
55
66
  end
56
67
 
68
+ # Rows gathered into one JSON document: json_arrayagg collects a value
69
+ # from each row into an array, json_objectagg a key and a value into an
70
+ # object.
71
+ def json_arrayagg(value)
72
+ AST::JsonAggregate.new(:arrayagg, [value])
73
+ end
74
+
75
+ def json_objectagg(key, value)
76
+ AST::JsonAggregate.new(:objectagg, [key, value])
77
+ end
78
+
79
+ # A JSON document built in the row: json_array from the values given,
80
+ # json_object from a Ruby hash whose values are expressions.
81
+ def json_array(*values)
82
+ AST::JsonBuild.new(:array, values)
83
+ end
84
+
85
+ def json_object(pairs = {})
86
+ AST::JsonBuild.new(:object, pairs)
87
+ end
88
+
57
89
  # Scalar functions, defined as real methods so that a typo is a
58
90
  # NoMethodError and a name Kernel also answers to (format, hash, test)
59
91
  # cannot quietly mean something else.
@@ -72,27 +104,27 @@ module ActiveRecord
72
104
  mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
73
105
  round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
74
106
  tan: {}, trim: {}, upper: {},
75
- char_length: {sqlite: 'LENGTH'},
76
- greatest: {sqlite: 'MAX'},
77
- least: {sqlite: 'MIN'},
107
+ char_length: { sqlite: "LENGTH" },
108
+ greatest: { sqlite: "MAX" },
109
+ least: { sqlite: "MIN" },
78
110
  # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
79
- log2: {postgresql: nil},
111
+ log2: { postgresql: nil },
80
112
  # MySQL's TRUNCATE insists on the second argument, where the others
81
113
  # default it to zero; SQLite's trunc takes only the one.
82
- trunc: {mysql: 'TRUNCATE'},
83
- now: {sqlite: nil},
114
+ trunc: { mysql: "TRUNCATE" },
115
+ now: { sqlite: nil },
84
116
  # The bit aggregates, which PostgreSQL and MySQL spell alike and
85
117
  # SQLite has none of. PostgreSQL gained bit_xor in 14.
86
- bit_and: {sqlite: nil}, bit_or: {sqlite: nil}, bit_xor: {sqlite: nil},
87
- date_trunc: {sqlite: nil, mysql: nil},
118
+ bit_and: { sqlite: nil }, bit_or: { sqlite: nil }, bit_xor: { sqlite: nil },
119
+ date_trunc: { sqlite: nil, mysql: nil },
88
120
  # Named for Kernel#rand, which it also takes back: a block calling
89
121
  # rand would otherwise get Ruby's and never reach the database.
90
- rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
122
+ rand: { sqlite: "RANDOM", postgresql: "RANDOM" },
91
123
  # Two different functions share this name: printf formatting here, and
92
124
  # on MySQL the one that puts separators in a number, which reads a
93
125
  # printf template as the number zero rather than complaining. The
94
126
  # name keeps the one meaning; fn(:format, ...) reaches MySQL's.
95
- format: {mysql: nil},
127
+ format: { mysql: nil },
96
128
  }.freeze
97
129
 
98
130
  SCALAR_FUNCTIONS.each_key do |name|
@@ -112,8 +144,8 @@ module ActiveRecord
112
144
  current_date: {},
113
145
  current_time: {},
114
146
  current_timestamp: {},
115
- localtime: {sqlite: nil},
116
- localtimestamp: {sqlite: nil},
147
+ localtime: { sqlite: nil },
148
+ localtimestamp: { sqlite: nil },
117
149
  }.freeze
118
150
 
119
151
  def current_date
@@ -150,11 +182,12 @@ module ActiveRecord
150
182
  node
151
183
  end
152
184
 
153
- # GROUP BY GROUPING SETS / ROLLUP / CUBE, which PostgreSQL has and the
154
- # others do not -- MySQL's WITH ROLLUP says one of the three and says it
155
- # somewhere else in the clause. Arel has the nodes and writes them for
156
- # PostgreSQL alone, so what it would raise elsewhere says nothing; this
157
- # says it here, as extract does, while the block is being read.
185
+ # GROUP BY GROUPING SETS / ROLLUP / CUBE. PostgreSQL has all three;
186
+ # the MySQL family has WITH ROLLUP, which says rollup and only rollup,
187
+ # trailing the group list -- the node spells it there. Arel has the
188
+ # nodes and writes them for PostgreSQL alone, so what it would raise
189
+ # elsewhere says nothing; this says it here, as extract does, while
190
+ # the block is being read.
158
191
  #
159
192
  # Sale.group { grouping_sets([:region], [:product], []) }
160
193
  # Sale.group { rollup(:region, :product) }
@@ -186,21 +219,21 @@ module ActiveRecord
186
219
  end
187
220
 
188
221
  %i[ntile first_value last_value].each do |name|
189
- define_method(name) {|arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
222
+ define_method(name) { |arg| AST::WindowFunction.new(name.to_s.upcase, [arg]) }
190
223
  end
191
224
 
192
225
  def nth_value(expr, nth)
193
- AST::WindowFunction.new('NTH_VALUE', [expr, nth])
226
+ AST::WindowFunction.new("NTH_VALUE", [expr, nth])
194
227
  end
195
228
 
196
229
  # The offset is written out rather than left to default, so that a
197
230
  # default value cannot end up where the offset belongs.
198
231
  def lag(expr, offset = 1, default = nil)
199
- AST::WindowFunction.new('LAG', default.nil? ? [expr, offset] : [expr, offset, default])
232
+ AST::WindowFunction.new("LAG", default.nil? ? [expr, offset] : [expr, offset, default])
200
233
  end
201
234
 
202
235
  def lead(expr, offset = 1, default = nil)
203
- AST::WindowFunction.new('LEAD', default.nil? ? [expr, offset] : [expr, offset, default])
236
+ AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default])
204
237
  end
205
238
 
206
239
  # Escape hatch for functions without a method of their own. The name is
@@ -212,15 +245,20 @@ module ActiveRecord
212
245
  AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
213
246
  end
214
247
 
248
+ # The same escape hatch for operators: op("&&", :tags, "{ruby,sql}").
249
+ def op(operator, left, right)
250
+ AST::Operation.new(operator, left, right)
251
+ end
252
+
215
253
  # BIT_COUNT. MySQL counts the bits of a number; PostgreSQL counts those
216
254
  # of a bit string, so the argument is cast, and to bit(64) because that
217
255
  # is what makes a negative come back as MySQL has it -- 64 bits of two's
218
256
  # complement rather than as many as the column happens to be wide.
219
257
  def bit_count(expr)
220
258
  case adapter_family
221
- when :mysql then AST::Function.new('BIT_COUNT', [expr])
259
+ when :mysql then AST::Function.new("BIT_COUNT", [expr])
222
260
  when :postgresql
223
- AST::Function.new('BIT_COUNT', [AST::Cast.new(expr, 'bit(64)')])
261
+ AST::Function.new("BIT_COUNT", [AST::Cast.new(expr, "bit(64)")])
224
262
  else
225
263
  raise NotImplementedError,
226
264
  "bit_count has no equivalent on #{@model.connection_db_config.adapter}"
@@ -240,20 +278,29 @@ module ActiveRecord
240
278
  # `== any` is IN and `!= all` is NOT IN, so what these add is the four
241
279
  # comparisons IN has no spelling for.
242
280
  def any(relation)
243
- quantified('ANY', relation)
281
+ quantified("ANY", relation)
244
282
  end
245
283
 
246
284
  def all(relation)
247
- quantified('ALL', relation)
285
+ quantified("ALL", relation)
286
+ end
287
+
288
+ # SQL as written, asked for by name:
289
+ #
290
+ # where { sql("length(name) > ?", 10) }
291
+ #
292
+ # The one way a string means SQL inside a block. ? and :name
293
+ # placeholders take quoted values, through sanitize_sql_array.
294
+ def sql(statement, *binds)
295
+ AST::Sql.new(statement, binds)
248
296
  end
249
297
 
250
298
  # A literal where an expression is expected, quoted like any other value:
251
299
  #
252
300
  # select { [:id, value(0).as(:depth)] }
253
301
  #
254
- # Needed because the top of a select list is Active Record's, and a bare
255
- # string there is SQL rather than a string. Numbers have a shorthand --
256
- # `0.as(:depth)` -- since nothing else could be meant by one.
302
+ # Numbers and strings have a shorthand -- `0.as(:depth)` -- so this is
303
+ # the spelling for the rest: true, nil, a date.
257
304
  def value(literal)
258
305
  AST::Value.new(literal)
259
306
  end
@@ -264,8 +311,8 @@ module ActiveRecord
264
311
  def excluded(column)
265
312
  return AST::Column.new(:excluded, column) unless adapter_family == :mysql
266
313
 
267
- quoted = @model.with_connection {|c| c.quote_column_name(column) }
268
- AST::Function.new('VALUES', [Arel::Nodes::SqlLiteral.new(quoted)])
314
+ quoted = @model.with_connection { |c| c.quote_column_name(column) }
315
+ AST::Function.new("VALUES", [Arel::Nodes::SqlLiteral.new(quoted)])
269
316
  end
270
317
 
271
318
  # CASE. `case` is a keyword, so Ruby only reaches this one through the
@@ -287,43 +334,42 @@ module ActiveRecord
287
334
  end
288
335
 
289
336
  private
290
-
291
- # SQLite is the one adapter with no quantifier at all, and what it says
292
- # when it meets one is a syntax error at the SELECT.
293
- def quantified(kind, relation)
294
- if adapter_family == :sqlite
295
- raise NotImplementedError,
296
- "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
337
+ # SQLite is the one adapter with no quantifier at all, and what it says
338
+ # when it meets one is a syntax error at the SELECT.
339
+ def quantified(kind, relation)
340
+ if adapter_family == :sqlite
341
+ raise NotImplementedError,
342
+ "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
343
+ end
344
+ AST::Quantified.new(kind, relation)
297
345
  end
298
- AST::Quantified.new(kind, relation)
299
- end
300
346
 
301
- def grouping(kind, sets)
302
- node = AST::GroupingSets.new(kind, sets)
303
- unless adapter_family == :postgresql
347
+ def grouping(kind, sets)
348
+ node = AST::GroupingSets.new(kind, sets)
349
+ return node if adapter_family == :postgresql
350
+ return node if kind == :rollup && adapter_family == :mysql
351
+
304
352
  raise NotImplementedError,
305
353
  "#{kind} has no equivalent on #{@model.connection_db_config.adapter}"
306
354
  end
307
- node
308
- end
309
355
 
310
- def function_name(name, functions)
311
- spellings = functions.fetch(name)
312
- return name.to_s.upcase unless spellings.key?(adapter_family)
313
- spellings.fetch(adapter_family) ||
314
- raise(NotImplementedError,
315
- "#{name} has no equivalent on #{@model.connection_db_config.adapter}")
316
- end
356
+ def function_name(name, functions)
357
+ spellings = functions.fetch(name)
358
+ return name.to_s.upcase unless spellings.key?(adapter_family)
359
+ spellings.fetch(adapter_family) ||
360
+ raise(NotImplementedError,
361
+ "#{name} has no equivalent on #{@model.connection_db_config.adapter}")
362
+ end
317
363
 
318
- def adapter_family
319
- @adapter_family ||= AST.adapter_family(@model)
320
- end
364
+ def adapter_family
365
+ @adapter_family ||= AST.adapter_family(@model)
366
+ end
321
367
  end
322
368
 
323
369
  module QueryMethods
324
370
  def where(opts = nil, *rest, &block)
325
371
  if block
326
- super(evaluate_block(&block).to_arel(table, klass))
372
+ super(to_arel_condition(evaluate_block(&block)))
327
373
  else
328
374
  super
329
375
  end
@@ -331,9 +377,7 @@ module ActiveRecord
331
377
 
332
378
  def select(*fields, &block)
333
379
  if block
334
- result = evaluate_block(&block)
335
- arel = Array(result).map {|node| to_arel_field(node) }
336
- super(*arel, &nil)
380
+ super(*to_arel_fields(evaluate_block(&block)), &nil)
337
381
  else
338
382
  super
339
383
  end
@@ -341,7 +385,7 @@ module ActiveRecord
341
385
 
342
386
  def having(opts = nil, *rest, &block)
343
387
  if block
344
- super(evaluate_block(&block).to_arel(table, klass))
388
+ super(to_arel_condition(evaluate_block(&block)))
345
389
  else
346
390
  super
347
391
  end
@@ -349,9 +393,7 @@ module ActiveRecord
349
393
 
350
394
  def order(*args, &block)
351
395
  if block
352
- result = evaluate_block(&block)
353
- arel = Array(result).map {|node| to_arel_field(node) }
354
- super(*arel, &nil)
396
+ super(*to_arel_fields(evaluate_block(&block)), &nil)
355
397
  else
356
398
  super
357
399
  end
@@ -360,8 +402,8 @@ module ActiveRecord
360
402
  def group(*args, &block)
361
403
  if block
362
404
  result = evaluate_block(&block)
363
- arel = Array(result).map {|node| to_arel_field(node) }
364
- super(*arel, &nil)
405
+ check_rollup_stands_alone(result)
406
+ super(*to_arel_fields(result), &nil)
365
407
  else
366
408
  super
367
409
  end
@@ -514,130 +556,169 @@ module ActiveRecord
514
556
  def cross_joins(*args, as: nil, &block)
515
557
  if block
516
558
  raise ArgumentError,
517
- 'a cross join has no condition; joins is the one that takes a block'
559
+ "a cross join has no condition; joins is the one that takes a block"
518
560
  end
519
561
  joins(build_cross_join(args.first, as))
520
562
  end
521
563
 
522
564
  private
523
-
524
- def build_arel(...)
525
- check_from_cte
526
- arel = super
527
- unless distinct_on_values.empty?
528
- arel.distinct_on(distinct_on_values.map {|column| to_arel_field(column) })
565
+ def build_arel(...)
566
+ check_from_cte
567
+ arel = super
568
+ unless distinct_on_values.empty?
569
+ arel.distinct_on(distinct_on_values.map { |column| to_arel_field(column) })
570
+ end
571
+ arel
529
572
  end
530
- arel
531
- end
532
573
 
533
- # Only when every `with` is one this can read the names out of; anything
534
- # else and there is nothing to be sure about, so nothing is said.
535
- def check_from_cte
536
- name = from_cte_value
537
- return unless name
538
- return unless with_values.all? {|value| value.is_a?(::Hash) }
574
+ # Only when every `with` is one this can read the names out of; anything
575
+ # else and there is nothing to be sure about, so nothing is said.
576
+ def check_from_cte
577
+ name = from_cte_value
578
+ return unless name
579
+ return unless with_values.all? { |value| value.is_a?(::Hash) }
539
580
 
540
- declared = with_values.flat_map {|value| value.keys.map(&:to_sym) }
541
- return if declared.include?(name)
542
-
543
- raise ArgumentError,
544
- "from_cte(#{name.inspect}) names no CTE; " +
545
- (declared.empty? ? "this query declares none" :
546
- "this query declares #{declared.map(&:inspect).join(', ')}")
547
- end
581
+ declared = with_values.flat_map { |value| value.keys.map(&:to_sym) }
582
+ return if declared.include?(name)
548
583
 
549
- def evaluate_block(&block)
550
- refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
551
- BlockContext.new(klass).instance_exec(&refined_block)
552
- end
584
+ raise ArgumentError,
585
+ "from_cte(#{name.inspect}) names no CTE; " +
586
+ (declared.empty? ? "this query declares none" :
587
+ "this query declares #{declared.map(&:inspect).join(', ')}")
588
+ end
553
589
 
554
- def to_arel_field(node)
555
- case node
556
- when AST::Node then node.to_arel(table, klass)
557
- when Symbol then table[node]
558
- else node
590
+ def evaluate_block(&block)
591
+ refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
592
+ BlockContext.new(klass).instance_exec(&refined_block)
559
593
  end
560
- end
561
594
 
562
- def reject_join_alias(alias_name)
563
- return unless alias_name
564
- raise ArgumentError, "as: needs a block to write the ON clause with"
565
- end
595
+ # WITH ROLLUP trails the whole group list, so on the MySQL family a
596
+ # rollup cannot stand beside other group entries the way PostgreSQL's
597
+ # ROLLUP(...) can.
598
+ def check_rollup_stands_alone(result)
599
+ entries = Array(result)
600
+ return if entries.size == 1
601
+ return unless entries.any? { |node| node.is_a?(AST::GroupingSets) }
602
+ return unless AST.adapter_family(klass) == :mysql
566
603
 
567
- # The subquery is written out rather than handed over as a tree: Arel has
568
- # a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
569
- # read what it will not write. Without a block the join is ON TRUE,
570
- # which is the usual shape -- what the subquery is allowed to see is
571
- # what makes it lateral, and that is said inside it.
572
- def build_lateral_join(relation, join_class, alias_name, &block)
573
- unless relation.lateral_value
574
604
  raise ArgumentError,
575
- "a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
605
+ "WITH ROLLUP takes the whole group list; group by the rollup alone"
576
606
  end
577
- unless alias_name
578
- raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
607
+
608
+ def to_arel_condition(result)
609
+ return result if result.is_a?(Arel::Nodes::SqlLiteral)
610
+ if result.is_a?(::String)
611
+ raise ArgumentError,
612
+ "#{result.inspect} is a string, not a condition; sql(...) " \
613
+ "writes one as SQL"
614
+ end
615
+ result.to_arel(table, klass)
616
+ end
617
+
618
+ # The top of a select, order or group list. A bare string is refused
619
+ # rather than passed to Active Record, where it would be SQL: inside a
620
+ # block a string is a value in every other position, and a literal
621
+ # whose meaning turns on where it stands is how an interpolation
622
+ # becomes an injection.
623
+ def to_arel_fields(result)
624
+ Array(result).map do |node|
625
+ if node.is_a?(::String) && !node.is_a?(Arel::Nodes::SqlLiteral)
626
+ raise ArgumentError,
627
+ "#{node.inspect} could mean SQL or a string; " \
628
+ "sql(...) says the SQL, value(...) the string"
629
+ end
630
+ to_arel_field(node)
631
+ end
579
632
  end
580
- check_lateral_support
581
633
 
582
- aliased = Arel::Nodes::TableAlias.new(
583
- Arel::Nodes::SqlLiteral.new("LATERAL (#{relation.to_sql})"), alias_name)
584
- on = block ? evaluate_block(&block).to_arel(table, klass) : Arel::Nodes::True.new
585
- join_class.new(aliased, Arel::Nodes::On.new(on))
586
- end
634
+ def to_arel_field(node)
635
+ case node
636
+ when AST::Sql then node.field_arel(klass)
637
+ when AST::Node then node.to_arel(table, klass)
638
+ when Symbol then table[node]
639
+ else node
640
+ end
641
+ end
587
642
 
588
- # PostgreSQL has LATERAL and so does MySQL, from 8.0.14. SQLite has
589
- # none, and neither has MariaDB, which answers to the same adapter as
590
- # MySQL. An adapter nobody has classified is left to say for itself.
591
- def check_lateral_support
592
- case AST.adapter_family(klass)
593
- when :sqlite
594
- refuse_lateral('sqlite3')
595
- when :mysql
596
- refuse_lateral('MariaDB') if klass.with_connection {|c| c.mariadb? }
643
+ def reject_join_alias(alias_name)
644
+ return unless alias_name
645
+ raise ArgumentError, "as: needs a block to write the ON clause with"
597
646
  end
598
- end
599
647
 
600
- def refuse_lateral(database)
601
- raise NotImplementedError, "a lateral join has no equivalent on #{database}"
602
- end
648
+ # The subquery is written out rather than handed over as a tree: Arel has
649
+ # a LATERAL node but only PostgreSQL's visitor writes it, and MySQL can
650
+ # read what it will not write. Without a block the join is ON TRUE,
651
+ # which is the usual shape -- what the subquery is allowed to see is
652
+ # what makes it lateral, and that is said inside it.
653
+ def build_lateral_join(relation, join_class, alias_name, &block)
654
+ unless relation.lateral_value
655
+ raise ArgumentError,
656
+ "a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
657
+ end
658
+ unless alias_name
659
+ raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
660
+ end
661
+ check_lateral_support
662
+
663
+ aliased = Arel::Nodes::TableAlias.new(
664
+ Arel::Nodes::SqlLiteral.new("LATERAL (#{relation.to_sql})"), alias_name)
665
+ on = block ? evaluate_block(&block).to_arel(table, klass) : Arel::Nodes::True.new
666
+ join_class.new(aliased, Arel::Nodes::On.new(on))
667
+ end
668
+
669
+ # PostgreSQL has LATERAL and so does MySQL, from 8.0.14. SQLite has
670
+ # none, and neither has MariaDB, which answers to the same adapter as
671
+ # MySQL. An adapter nobody has classified is left to say for itself.
672
+ def check_lateral_support
673
+ case AST.adapter_family(klass)
674
+ when :sqlite
675
+ refuse_lateral("sqlite3")
676
+ when :mysql
677
+ refuse_lateral("MariaDB") if klass.with_connection { |c| c.mariadb? }
678
+ end
679
+ end
603
680
 
604
- # MySQL has no FULL OUTER JOIN, and neither has MariaDB; SQLite has had
605
- # one since 3.39 and PostgreSQL always.
606
- def check_full_outer_support
607
- return unless AST.adapter_family(klass) == :mysql
608
- raise NotImplementedError, 'a full outer join has no equivalent on MySQL'
609
- end
681
+ def refuse_lateral(database)
682
+ raise NotImplementedError, "a lateral join has no equivalent on #{database}"
683
+ end
610
684
 
611
- def outer_joins(called, join_class, args, alias_name, &block)
612
- if args.first.is_a?(ActiveRecord::Relation)
613
- return joins(build_lateral_join(args.first, join_class, alias_name, &block))
685
+ # MySQL has no FULL OUTER JOIN, and neither has MariaDB; SQLite has had
686
+ # one since 3.39 and PostgreSQL always.
687
+ def check_full_outer_support
688
+ return unless AST.adapter_family(klass) == :mysql
689
+ raise NotImplementedError, "a full outer join has no equivalent on MySQL"
614
690
  end
615
- return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
616
691
 
617
- raise ArgumentError,
618
- "#{called} takes a table and the block that joins it; an association " \
619
- 'is what joins and left_outer_joins read'
620
- end
692
+ def outer_joins(called, join_class, args, alias_name, &block)
693
+ if args.first.is_a?(ActiveRecord::Relation)
694
+ return joins(build_lateral_join(args.first, join_class, alias_name, &block))
695
+ end
696
+ return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
621
697
 
622
- # Arel has a node for every other join and none for this one, and INNER
623
- # JOIN with no ON -- which is a cross join on SQLite and MySQL -- is a
624
- # syntax error on PostgreSQL. So the SQL is written here, the second
625
- # place in the gem that writes any: the keyword is fixed and the names
626
- # are quoted by the adapter, so nothing of the caller's is in it.
627
- def build_cross_join(target_table, alias_name)
628
- joined = klass.with_connection do |connection|
629
- name = connection.quote_table_name(target_table.to_s)
630
- alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
698
+ raise ArgumentError,
699
+ "#{called} takes a table and the block that joins it; an association " \
700
+ "is what joins and left_outer_joins read"
701
+ end
702
+
703
+ # Arel has a node for every other join and none for this one, and INNER
704
+ # JOIN with no ON -- which is a cross join on SQLite and MySQL -- is a
705
+ # syntax error on PostgreSQL. So the SQL is written here, the second
706
+ # place in the gem that writes any: the keyword is fixed and the names
707
+ # are quoted by the adapter, so nothing of the caller's is in it.
708
+ def build_cross_join(target_table, alias_name)
709
+ joined = klass.with_connection do |connection|
710
+ name = connection.quote_table_name(target_table.to_s)
711
+ alias_name ? "#{name} #{connection.quote_table_name(alias_name.to_s)}" : name
712
+ end
713
+ Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
631
714
  end
632
- Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
633
- end
634
715
 
635
- def build_join_node(target_table, join_class, alias_name, &block)
636
- ast = evaluate_block(&block)
637
- arel_table = Arel::Table.new(target_table)
638
- arel_table = arel_table.alias(alias_name) if alias_name
639
- join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table, klass)))
640
- end
716
+ def build_join_node(target_table, join_class, alias_name, &block)
717
+ ast = evaluate_block(&block)
718
+ arel_table = Arel::Table.new(target_table)
719
+ arel_table = arel_table.alias(alias_name) if alias_name
720
+ join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table, klass)))
721
+ end
641
722
  end
642
723
 
643
724
  # The writing statements, which live on Relation rather than in
@@ -658,7 +739,7 @@ module ActiveRecord
658
739
  unless result.is_a?(::Hash)
659
740
  raise ArgumentError, "the block gives update_all a hash of column => value"
660
741
  end
661
- super(result.transform_values {|value| to_arel_field(value) })
742
+ super(result.transform_values { |value| to_arel_field(value) })
662
743
  end
663
744
 
664
745
  # upsert_all's on_duplicate takes SQL text and nothing else, so this is
@@ -684,19 +765,18 @@ module ActiveRecord
684
765
  end
685
766
 
686
767
  private
687
-
688
- # The left of each assignment is the column being written, which is bare
689
- # -- the statement is already about one table -- and the right is the
690
- # expression, compiled here because a string is what on_duplicate reads.
691
- def set_clause(updates)
692
- klass.with_connection do |connection|
693
- updates.map do |column, value|
694
- expression = connection.visitor.compile(
695
- to_arel_field(value), Arel::Collectors::SQLString.new)
696
- "#{connection.quote_column_name(column)}=#{expression}"
697
- end.join(', ')
768
+ # The left of each assignment is the column being written, which is bare
769
+ # -- the statement is already about one table -- and the right is the
770
+ # expression, compiled here because a string is what on_duplicate reads.
771
+ def set_clause(updates)
772
+ klass.with_connection do |connection|
773
+ updates.map do |column, value|
774
+ expression = connection.visitor.compile(
775
+ to_arel_field(value), Arel::Collectors::SQLString.new)
776
+ "#{connection.quote_column_name(column)}=#{expression}"
777
+ end.join(", ")
778
+ end
698
779
  end
699
- end
700
780
  end
701
781
  end
702
782
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Activerecord
2
4
  module Refined
3
- VERSION = '0.8.0'
5
+ VERSION = "0.9.0"
4
6
  end
5
7
  end