activerecord-refined 0.6.1 → 0.8.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.
- checksums.yaml +4 -4
- data/.github/workflows/sandbox.yml +1 -1
- data/README.md +114 -35
- data/activerecord-refined.gemspec +4 -2
- data/benchmark/query_building.rb +1 -1
- data/examples/complex_joins.rb +30 -0
- data/examples/ctes.rb +2 -2
- data/examples/json.rb +45 -17
- data/examples/postgresql.rb +1 -1
- data/examples/subqueries.rb +1 -1
- data/examples/windows.rb +1 -1
- data/examples/writes.rb +4 -4
- data/lib/active_record/refined/ast.rb +155 -20
- data/lib/active_record/refined.rb +94 -13
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +5 -3
- data/test/test_block_syntax.rb +214 -29
- data/test/test_helper.rb +6 -1
- metadata +19 -3
|
@@ -217,15 +217,23 @@ module ActiveRecord
|
|
|
217
217
|
|
|
218
218
|
# Reading inside a JSON document, by the name of what Hash does. A
|
|
219
219
|
# string or symbol steps into an object, an integer into an array, and
|
|
220
|
-
# what comes back is
|
|
221
|
-
#
|
|
222
|
-
#
|
|
220
|
+
# what comes back is still JSON, the way Hash#dig hands back the
|
|
221
|
+
# structure itself -- for a document to be dug into further or asked
|
|
222
|
+
# the JSON questions. dig_text gives the value as text instead,
|
|
223
|
+
# which is what a comparison wants.
|
|
223
224
|
def dig(*path)
|
|
225
|
+
JsonPath.new(self, path, as_json: true)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def dig_text(*path)
|
|
224
229
|
JsonPath.new(self, path)
|
|
225
230
|
end
|
|
226
231
|
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
# Keys taken out of a JSON document, by the name of what Hash does,
|
|
233
|
+
# and taking keys as Hash#except takes them. Like bury it gives back
|
|
234
|
+
# the document changed rather than writing it anywhere.
|
|
235
|
+
def except(*keys)
|
|
236
|
+
JsonExcept.new(self, keys)
|
|
229
237
|
end
|
|
230
238
|
|
|
231
239
|
# What dig reads, bury sets: the last argument is the value and the
|
|
@@ -359,7 +367,7 @@ module ActiveRecord
|
|
|
359
367
|
# A literal standing where an expression would: `select { value(0).as(:depth) }`.
|
|
360
368
|
#
|
|
361
369
|
# Values reach the SQL quoted wherever they appear as an operand, but the
|
|
362
|
-
# top of a select list is
|
|
370
|
+
# top of a select list is Active Record's, and a bare string there is SQL
|
|
363
371
|
# rather than a string. Saying `value` is how you ask for the other
|
|
364
372
|
# meaning, and it carries the predications with it, so a literal can be
|
|
365
373
|
# compared and combined like anything else.
|
|
@@ -441,7 +449,7 @@ module ActiveRecord
|
|
|
441
449
|
end
|
|
442
450
|
|
|
443
451
|
# What a `when` is until its `then` arrives. A Node so that using it
|
|
444
|
-
# as one says what is missing rather than reaching
|
|
452
|
+
# as one says what is missing rather than reaching Active Record as
|
|
445
453
|
# something it cannot read.
|
|
446
454
|
class Pending < Node
|
|
447
455
|
def initialize(kase, condition)
|
|
@@ -474,20 +482,23 @@ module ActiveRecord
|
|
|
474
482
|
end
|
|
475
483
|
|
|
476
484
|
# 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
|
-
|
|
479
|
-
|
|
485
|
+
# quoted so that a comma or a brace in a key is part of it. except
|
|
486
|
+
# writes its keys the same way, which are steps of no one path.
|
|
487
|
+
def steps_array(steps = path)
|
|
488
|
+
"{#{steps.map {|step| %("#{escape_step(step)}") }.join(',')}}"
|
|
480
489
|
end
|
|
481
490
|
|
|
482
491
|
# MySQL and SQLite take a path expression instead, where an integer is
|
|
483
492
|
# a subscript and a name that is not plain has to be quoted.
|
|
484
493
|
def dollar_path
|
|
485
|
-
path.inject(+'$')
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
494
|
+
path.inject(+'$') {|so_far, step| so_far << dollar_step(step) }
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def dollar_step(step)
|
|
498
|
+
return "[#{step}]" if step.is_a?(::Integer)
|
|
499
|
+
name = step.to_s
|
|
500
|
+
'.' + (name.match?(/\A[[:alpha:]_][[:alnum:]_]*\z/) ?
|
|
501
|
+
name : %("#{escape_step(step)}"))
|
|
491
502
|
end
|
|
492
503
|
|
|
493
504
|
def escape_step(step)
|
|
@@ -503,10 +514,85 @@ module ActiveRecord
|
|
|
503
514
|
#
|
|
504
515
|
# The path is turned into a string either way, so a key with a space or
|
|
505
516
|
# a quote in it travels as itself rather than having to be refused.
|
|
517
|
+
# What a dug value may be compared with. dig_text gives text on every
|
|
518
|
+
# adapter, and what a text value compared with a number means is a
|
|
519
|
+
# question the three answer three ways: `dig_text(:n) == 5` is true on
|
|
520
|
+
# SQLite, an error on PostgreSQL and true on MySQL, while
|
|
521
|
+
# `dig_text(:flag) == true` is true, an error, and false. cast is what
|
|
522
|
+
# says which type was meant, and then all three agree.
|
|
523
|
+
#
|
|
524
|
+
# dig is refused the other way about: the JSON for a string carries
|
|
525
|
+
# its quotes, so `dig(:name) == 'alice'` is false on SQLite, an
|
|
526
|
+
# error on PostgreSQL and true on MySQL. dig_text is the one that
|
|
527
|
+
# gives the value.
|
|
528
|
+
#
|
|
529
|
+
# A string against dig_text, and anything the block itself built -- a
|
|
530
|
+
# column, a function, another dug value -- go through untouched.
|
|
531
|
+
module JsonComparable
|
|
532
|
+
%i[== != < <= > >=].each do |operator|
|
|
533
|
+
define_method(operator) do |other|
|
|
534
|
+
check_comparable(other)
|
|
535
|
+
super(other)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def in?(values) = super(check_each(values))
|
|
540
|
+
def not_in?(values) = super(check_each(values))
|
|
541
|
+
def between?(min, max) = super(*check_each([min, max]))
|
|
542
|
+
def not_between?(min, max) = super(*check_each([min, max]))
|
|
543
|
+
|
|
544
|
+
private
|
|
545
|
+
|
|
546
|
+
# nil is left to the comparison itself, which says to use null?, and
|
|
547
|
+
# so is anything the block built rather than wrote as a literal.
|
|
548
|
+
def check_comparable(other)
|
|
549
|
+
return if other.nil? || other.is_a?(Node) || other.is_a?(::Symbol) ||
|
|
550
|
+
other.is_a?(Arel::Nodes::Node) ||
|
|
551
|
+
other.is_a?(Arel::Attributes::Attribute) ||
|
|
552
|
+
other.is_a?(ActiveRecord::Relation)
|
|
553
|
+
return if other.is_a?(::String) && !as_json
|
|
554
|
+
|
|
555
|
+
raise ArgumentError, as_json ?
|
|
556
|
+
"dig gives JSON, and comparing it with #{other.inspect} " \
|
|
557
|
+
"means something different on every adapter; dig_text gives the value" :
|
|
558
|
+
"dig_text gives text, and comparing it with #{other.inspect} means " \
|
|
559
|
+
"something different on every adapter; cast it to the type meant"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def check_each(values)
|
|
563
|
+
case values
|
|
564
|
+
when ActiveRecord::Relation then values
|
|
565
|
+
when ::Range then [values.begin, values.end].each {|v| check_comparable(v) }
|
|
566
|
+
else values.each {|value| check_comparable(value) }
|
|
567
|
+
end
|
|
568
|
+
values
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# The JSON operations read a document, and what dig gives is one:
|
|
573
|
+
# `dig(:author).key?(:email)` and `dig(:tags).contains?(...)` are the
|
|
574
|
+
# same question asked of a part of it, and the adapters answer them
|
|
575
|
+
# alike. What dig_text gives is text, and reading that as a document
|
|
576
|
+
# again is where they part company: SQLite parses it back and MySQL
|
|
577
|
+
# takes it as written, where PostgreSQL has no such function for text.
|
|
578
|
+
module JsonDocument
|
|
579
|
+
%i[dig dig_text key? contains? bury except].each do |name|
|
|
580
|
+
define_method(name) do |*args|
|
|
581
|
+
unless as_json
|
|
582
|
+
raise ArgumentError,
|
|
583
|
+
"dig_text gives text, and #{name} reads JSON; dig keeps it"
|
|
584
|
+
end
|
|
585
|
+
super(*args)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
506
590
|
class JsonPath < Node
|
|
507
591
|
include Predications
|
|
508
592
|
include Arithmetics
|
|
509
593
|
include JsonSteps
|
|
594
|
+
include JsonComparable
|
|
595
|
+
include JsonDocument
|
|
510
596
|
|
|
511
597
|
attr_reader :operand, :path, :as_json
|
|
512
598
|
|
|
@@ -530,8 +616,8 @@ module ActiveRecord
|
|
|
530
616
|
extracted = Arel::Nodes::InfixOperation.new(
|
|
531
617
|
as_json ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
|
|
532
618
|
# SQLite's ->> gives back the value with its type, where the other
|
|
533
|
-
# two give text. Cast so that `
|
|
534
|
-
# thing everywhere, and a number wants a cast everywhere too.
|
|
619
|
+
# two give text. Cast so that `dig_text(:n) == '5'` means the
|
|
620
|
+
# same thing everywhere, and a number wants a cast everywhere too.
|
|
535
621
|
as_json ? extracted : Arel::Nodes::NamedFunction.new(
|
|
536
622
|
'CAST', [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new('text'))])
|
|
537
623
|
end
|
|
@@ -595,6 +681,55 @@ module ActiveRecord
|
|
|
595
681
|
end
|
|
596
682
|
end
|
|
597
683
|
|
|
684
|
+
# Keys taken out of a JSON document. PostgreSQL subtracts them, the
|
|
685
|
+
# other two remove a path apiece.
|
|
686
|
+
class JsonExcept < Node
|
|
687
|
+
include Predications
|
|
688
|
+
include JsonSteps
|
|
689
|
+
|
|
690
|
+
attr_reader :operand, :keys
|
|
691
|
+
|
|
692
|
+
def initialize(operand, keys)
|
|
693
|
+
@operand = operand
|
|
694
|
+
@keys = check_keys(keys)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def to_arel(table, model)
|
|
698
|
+
document = to_arel_operand(operand, table, model)
|
|
699
|
+
return Arel::Nodes::InfixOperation.new(:-, document, key_array) if
|
|
700
|
+
AST.adapter_family(model) == :postgresql
|
|
701
|
+
|
|
702
|
+
Arel::Nodes::NamedFunction.new(
|
|
703
|
+
'JSON_REMOVE',
|
|
704
|
+
[document, *keys.map {|key| Arel::Nodes.build_quoted("$#{dollar_step(key)}") }])
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
private
|
|
708
|
+
|
|
709
|
+
# jsonb has three subtractions -- a key, an array of keys, an element
|
|
710
|
+
# by index -- and an array literal written without a type is read as
|
|
711
|
+
# the first of them: `meta - '{draft}'` takes out the key spelled
|
|
712
|
+
# {draft}, which is nothing, and says nothing about it.
|
|
713
|
+
def key_array
|
|
714
|
+
Arel::Nodes::NamedFunction.new(
|
|
715
|
+
'CAST',
|
|
716
|
+
[Arel::Nodes::As.new(Arel::Nodes.build_quoted(steps_array(keys)),
|
|
717
|
+
Arel::Nodes::SqlLiteral.new('text[]'))])
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Keys, as Hash#except takes them: an index into an array is not what
|
|
721
|
+
# the name says anywhere, and is bury's business through a path.
|
|
722
|
+
def check_keys(keys)
|
|
723
|
+
raise ArgumentError, 'except needs a key' if keys.empty?
|
|
724
|
+
keys.each do |key|
|
|
725
|
+
next if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
726
|
+
raise ArgumentError,
|
|
727
|
+
"except takes keys of the document, not #{key.inspect}"
|
|
728
|
+
end
|
|
729
|
+
keys
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
598
733
|
# JSON containment: whether the document holds what is given.
|
|
599
734
|
class JsonContains < Predicate
|
|
600
735
|
attr_reader :operand, :value
|
|
@@ -1128,7 +1263,7 @@ module ActiveRecord
|
|
|
1128
1263
|
|
|
1129
1264
|
# A plain SQL comparison. The value is passed through as it is, so a Range
|
|
1130
1265
|
# or an Array compares against a PostgreSQL range or array column, the way
|
|
1131
|
-
#
|
|
1266
|
+
# Active Record's own force_equality? types do.
|
|
1132
1267
|
class Comparison < Predicate
|
|
1133
1268
|
OPERATOR_MAP = {
|
|
1134
1269
|
:== => :eq, :!= => :not_eq,
|
|
@@ -1187,7 +1322,7 @@ module ActiveRecord
|
|
|
1187
1322
|
end
|
|
1188
1323
|
|
|
1189
1324
|
# A relation standing for a set of values, which is what IN and the
|
|
1190
|
-
# quantifiers each take. The treatment is
|
|
1325
|
+
# quantifiers each take. The treatment is Active Record's own
|
|
1191
1326
|
# RelationHandler's: without an explicit select list the subquery
|
|
1192
1327
|
# selects the model's primary key.
|
|
1193
1328
|
module SetSubquery
|
|
@@ -251,7 +251,7 @@ module ActiveRecord
|
|
|
251
251
|
#
|
|
252
252
|
# select { [:id, value(0).as(:depth)] }
|
|
253
253
|
#
|
|
254
|
-
# Needed because the top of a select list is
|
|
254
|
+
# Needed because the top of a select list is Active Record's, and a bare
|
|
255
255
|
# string there is SQL rather than a string. Numbers have a shorthand --
|
|
256
256
|
# `0.as(:depth)` -- since nothing else could be meant by one.
|
|
257
257
|
def value(literal)
|
|
@@ -367,7 +367,7 @@ module ActiveRecord
|
|
|
367
367
|
end
|
|
368
368
|
end
|
|
369
369
|
|
|
370
|
-
# A symbol names a table, which
|
|
370
|
+
# A symbol names a table, which Active Record's own from only takes as a
|
|
371
371
|
# string. With `as` it is selected under another name; when that name
|
|
372
372
|
# is the model's own, from_cte says the same thing without repeating it.
|
|
373
373
|
def from(value, subquery_name = nil, as: nil)
|
|
@@ -383,7 +383,7 @@ module ActiveRecord
|
|
|
383
383
|
end
|
|
384
384
|
|
|
385
385
|
# Selects a CTE in place of the model's own table. The alias is not a
|
|
386
|
-
# choice --
|
|
386
|
+
# choice -- Active Record keeps qualifying columns with the table name,
|
|
387
387
|
# so the model's is the only name that works -- which is why it is
|
|
388
388
|
# taken from the model rather than asked for:
|
|
389
389
|
# with_recursive(tree: [...]).from_cte(:tree)
|
|
@@ -432,7 +432,7 @@ module ActiveRecord
|
|
|
432
432
|
self
|
|
433
433
|
end
|
|
434
434
|
|
|
435
|
-
#
|
|
435
|
+
# Active Record generates these for the values it knows about; this one
|
|
436
436
|
# is ours, and lives in the same place so that it survives a spawn.
|
|
437
437
|
def distinct_on_values
|
|
438
438
|
@values.fetch(:distinct_on, ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY)
|
|
@@ -443,13 +443,32 @@ module ActiveRecord
|
|
|
443
443
|
@values[:distinct_on] = columns
|
|
444
444
|
end
|
|
445
445
|
|
|
446
|
+
# Marks the relation for a lateral join, which lets it see the row being
|
|
447
|
+
# joined to -- the top few rows of each group, and the like. In SQL the
|
|
448
|
+
# keyword modifies the subquery, not the join, so it is said on the
|
|
449
|
+
# relation: left_outer_joins(top_post.lateral, as: :top).
|
|
450
|
+
def lateral
|
|
451
|
+
spawn.lateral!
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def lateral!
|
|
455
|
+
self.lateral_value = true
|
|
456
|
+
self
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def lateral_value
|
|
460
|
+
@values[:lateral]
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def lateral_value=(value)
|
|
464
|
+
assert_modifiable!
|
|
465
|
+
@values[:lateral] = value
|
|
466
|
+
end
|
|
467
|
+
|
|
446
468
|
# `as` names the table within the query, which is what makes a self
|
|
447
469
|
# join expressible: joins(:employees, as: :managers) { ... }.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
# being joined to -- the top few rows of each group, and the like.
|
|
451
|
-
def joins(*args, as: nil, lateral: false, &block)
|
|
452
|
-
if lateral
|
|
470
|
+
def joins(*args, as: nil, &block)
|
|
471
|
+
if args.first.is_a?(ActiveRecord::Relation)
|
|
453
472
|
super(build_lateral_join(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
454
473
|
elsif block
|
|
455
474
|
super(build_join_node(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
@@ -459,8 +478,8 @@ module ActiveRecord
|
|
|
459
478
|
end
|
|
460
479
|
end
|
|
461
480
|
|
|
462
|
-
def left_outer_joins(*args, as: nil,
|
|
463
|
-
if
|
|
481
|
+
def left_outer_joins(*args, as: nil, &block)
|
|
482
|
+
if args.first.is_a?(ActiveRecord::Relation)
|
|
464
483
|
joins(build_lateral_join(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
465
484
|
elsif block
|
|
466
485
|
joins(build_join_node(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
@@ -470,6 +489,36 @@ module ActiveRecord
|
|
|
470
489
|
end
|
|
471
490
|
end
|
|
472
491
|
|
|
492
|
+
# The other two outer joins, which Active Record has no method for and
|
|
493
|
+
# Arel has the nodes for. The rules are joins': the block is the ON,
|
|
494
|
+
# `as` names the table within the query, a relation marked `lateral`
|
|
495
|
+
# joins as one. An association name is not among them -- what Active
|
|
496
|
+
# Record reads out of one is an inner or a left join and nothing else.
|
|
497
|
+
def right_outer_joins(*args, as: nil, &block)
|
|
498
|
+
outer_joins(:right_outer_joins, Arel::Nodes::RightOuterJoin,
|
|
499
|
+
args, as, &block)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def full_outer_joins(*args, as: nil, &block)
|
|
503
|
+
check_full_outer_support
|
|
504
|
+
outer_joins(:full_outer_joins, Arel::Nodes::FullOuterJoin,
|
|
505
|
+
args, as, &block)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# CROSS JOIN: every row of one table against every row of the other, so
|
|
509
|
+
# unlike the joins above there is no condition to give and no block to
|
|
510
|
+
# write it in.
|
|
511
|
+
#
|
|
512
|
+
# Post.cross_joins(:authors)
|
|
513
|
+
# Post.cross_joins(:posts, as: :others)
|
|
514
|
+
def cross_joins(*args, as: nil, &block)
|
|
515
|
+
if block
|
|
516
|
+
raise ArgumentError,
|
|
517
|
+
'a cross join has no condition; joins is the one that takes a block'
|
|
518
|
+
end
|
|
519
|
+
joins(build_cross_join(args.first, as))
|
|
520
|
+
end
|
|
521
|
+
|
|
473
522
|
private
|
|
474
523
|
|
|
475
524
|
def build_arel(...)
|
|
@@ -521,8 +570,9 @@ module ActiveRecord
|
|
|
521
570
|
# which is the usual shape -- what the subquery is allowed to see is
|
|
522
571
|
# what makes it lateral, and that is said inside it.
|
|
523
572
|
def build_lateral_join(relation, join_class, alias_name, &block)
|
|
524
|
-
unless relation.
|
|
525
|
-
raise ArgumentError,
|
|
573
|
+
unless relation.lateral_value
|
|
574
|
+
raise ArgumentError,
|
|
575
|
+
"a relation joins laterally; mark it: joins(sub.lateral, as: :top)"
|
|
526
576
|
end
|
|
527
577
|
unless alias_name
|
|
528
578
|
raise ArgumentError, "a lateral join needs a name: joins(..., as: :top)"
|
|
@@ -551,6 +601,37 @@ module ActiveRecord
|
|
|
551
601
|
raise NotImplementedError, "a lateral join has no equivalent on #{database}"
|
|
552
602
|
end
|
|
553
603
|
|
|
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
|
|
610
|
+
|
|
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))
|
|
614
|
+
end
|
|
615
|
+
return joins(build_join_node(args.first, join_class, alias_name, &block)) if block
|
|
616
|
+
|
|
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
|
|
621
|
+
|
|
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
|
|
631
|
+
end
|
|
632
|
+
Arel::Nodes::StringJoin.new(Arel.sql("CROSS JOIN #{joined}"))
|
|
633
|
+
end
|
|
634
|
+
|
|
554
635
|
def build_join_node(target_table, join_class, alias_name, &block)
|
|
555
636
|
ast = evaluate_block(&block)
|
|
556
637
|
arel_table = Arel::Table.new(target_table)
|
data/lib/activerecord-refined.rb
CHANGED
|
@@ -9,6 +9,8 @@ ActiveRecord::QueryMethods.prepend ActiveRecord::Refined::QueryMethods
|
|
|
9
9
|
# update_all and its kind are Relation's own rather than QueryMethods'.
|
|
10
10
|
ActiveRecord::Relation.prepend ActiveRecord::Refined::Writes
|
|
11
11
|
|
|
12
|
-
# The methods above are
|
|
13
|
-
# to its relation. These
|
|
14
|
-
ActiveRecord::Base.singleton_class.delegate
|
|
12
|
+
# The methods above are Active Record's own, so a model already forwards them
|
|
13
|
+
# to its relation. These are new, and have to be added to that list.
|
|
14
|
+
ActiveRecord::Base.singleton_class.delegate(
|
|
15
|
+
:from_cte, :distinct_on, :lateral,
|
|
16
|
+
:right_outer_joins, :full_outer_joins, :cross_joins, to: :all)
|