activerecord-refined 0.3.3 → 0.4.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/test.yml +56 -2
- data/LICENSE.txt +2 -1
- data/README.md +219 -17
- data/benchmark/query_building.rb +129 -0
- data/examples/complex_joins.rb +14 -1
- data/examples/ctes.rb +82 -0
- data/examples/expressions.rb +87 -0
- data/examples/postgresql.rb +105 -0
- data/examples/predicates.rb +93 -0
- data/examples/subqueries.rb +67 -0
- data/lib/active_record/refined/ast.rb +238 -27
- data/lib/active_record/refined.rb +115 -20
- data/lib/activerecord-refined/version.rb +1 -1
- data/test/test_block_syntax.rb +428 -2
- data/test/test_helper.rb +12 -0
- metadata +7 -1
|
@@ -1,6 +1,20 @@
|
|
|
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
|
+
|
|
13
|
+
def self.check_name(name, pattern, what)
|
|
14
|
+
return name if pattern.match?(name.to_s)
|
|
15
|
+
raise ArgumentError, "#{name.inspect} is not a plain #{what}"
|
|
16
|
+
end
|
|
17
|
+
|
|
4
18
|
# Predicate builders shared by symbols, qualified columns and
|
|
5
19
|
# expressions. Imported into the Symbol refinement with
|
|
6
20
|
# Refinement#import_methods, so every method must be defined with def.
|
|
@@ -62,6 +76,30 @@ module ActiveRecord
|
|
|
62
76
|
Like.new(self, pattern)
|
|
63
77
|
end
|
|
64
78
|
|
|
79
|
+
def ilike?(pattern)
|
|
80
|
+
Like.new(self, pattern, nil, case_sensitive: false)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Case-insensitive equality, folded on both sides rather than left to
|
|
84
|
+
# the collation, so it means the same thing on every adapter.
|
|
85
|
+
def casecmp?(value)
|
|
86
|
+
if value.nil?
|
|
87
|
+
raise ArgumentError, "casecmp? does not take nil; use null? instead"
|
|
88
|
+
end
|
|
89
|
+
Comparison.new(Function.new("LOWER", [self]), :==,
|
|
90
|
+
Function.new("LOWER", [value]))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Null-safe comparison: unlike = and <>, these treat NULL as a value,
|
|
94
|
+
# so not_distinct_from? is the one equality that may take nil.
|
|
95
|
+
def distinct_from?(value)
|
|
96
|
+
DistinctFrom.new(self, value, negated: true)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def not_distinct_from?(value)
|
|
100
|
+
DistinctFrom.new(self, value)
|
|
101
|
+
end
|
|
102
|
+
|
|
65
103
|
def start_with?(*prefixes)
|
|
66
104
|
if prefixes.empty?
|
|
67
105
|
raise ArgumentError, "start_with? needs at least one prefix"
|
|
@@ -80,8 +118,75 @@ module ActiveRecord
|
|
|
80
118
|
Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
|
|
81
119
|
end
|
|
82
120
|
|
|
121
|
+
# The array comparisons carry the meaning of their Ruby namesakes.
|
|
122
|
+
# member? is Enumerable's element test, so an Array argument is
|
|
123
|
+
# rejected rather than quietly meaning something Array#member? does
|
|
124
|
+
# not; whole-array comparisons go by the Set and Array names.
|
|
83
125
|
def member?(element)
|
|
84
|
-
|
|
126
|
+
if element.is_a?(::Array) || element.is_a?(::Set)
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"member? takes a single element; use superset? to require every element"
|
|
129
|
+
end
|
|
130
|
+
ArrayPredicate.new(self, :"@>", [element])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def superset?(elements)
|
|
134
|
+
ArrayPredicate.new(self, :"@>", ArrayPredicate.elements(elements, "superset?"))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def subset?(elements)
|
|
138
|
+
ArrayPredicate.new(self, :"<@", ArrayPredicate.elements(elements, "subset?"))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def intersect?(elements)
|
|
142
|
+
ArrayPredicate.new(self, :"&&", ArrayPredicate.elements(elements, "intersect?"))
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Arithmetic builders shared by symbols, qualified columns and
|
|
147
|
+
# expressions. Imported into the Symbol refinement like Predications,
|
|
148
|
+
# so every method must be defined with def.
|
|
149
|
+
module Arithmetics
|
|
150
|
+
def +(other)
|
|
151
|
+
Arithmetic.new(self, :+, other)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def -(other)
|
|
155
|
+
Arithmetic.new(self, :-, other)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def *(other)
|
|
159
|
+
Arithmetic.new(self, :*, other)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def /(other)
|
|
163
|
+
Arithmetic.new(self, :/, other)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Aggregate builders shared by symbols, qualified columns and
|
|
168
|
+
# expressions. Imported into the Symbol refinement like Predications,
|
|
169
|
+
# so every method must be defined with def.
|
|
170
|
+
module Aggregations
|
|
171
|
+
# DISTINCT is Arel's only aggregate modifier, and only for count.
|
|
172
|
+
def count(distinct: false)
|
|
173
|
+
Aggregate.new(self, :count, distinct: distinct)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def sum
|
|
177
|
+
Aggregate.new(self, :sum)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def average
|
|
181
|
+
Aggregate.new(self, :average)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def maximum
|
|
185
|
+
Aggregate.new(self, :maximum)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def minimum
|
|
189
|
+
Aggregate.new(self, :minimum)
|
|
85
190
|
end
|
|
86
191
|
end
|
|
87
192
|
|
|
@@ -131,6 +236,8 @@ module ActiveRecord
|
|
|
131
236
|
|
|
132
237
|
class Column < Node
|
|
133
238
|
include Predications
|
|
239
|
+
include Arithmetics
|
|
240
|
+
include Aggregations
|
|
134
241
|
|
|
135
242
|
attr_reader :table_name, :column_name
|
|
136
243
|
|
|
@@ -142,24 +249,55 @@ module ActiveRecord
|
|
|
142
249
|
def to_arel(_table)
|
|
143
250
|
Arel::Table.new(table_name)[column_name]
|
|
144
251
|
end
|
|
252
|
+
end
|
|
145
253
|
|
|
146
|
-
|
|
147
|
-
|
|
254
|
+
# Arithmetic on columns and expressions. Ruby's precedence puts these
|
|
255
|
+
# above the comparison operators, so :price * :quantity > 100 groups the
|
|
256
|
+
# way it reads.
|
|
257
|
+
class Arithmetic < Node
|
|
258
|
+
include Predications
|
|
259
|
+
include Arithmetics
|
|
260
|
+
include Aggregations
|
|
261
|
+
|
|
262
|
+
attr_reader :left, :operator, :right
|
|
263
|
+
|
|
264
|
+
def initialize(left, operator, right)
|
|
265
|
+
@left = left
|
|
266
|
+
@operator = operator
|
|
267
|
+
@right = right
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def to_arel(table)
|
|
271
|
+
to_arel_operand(left, table).
|
|
272
|
+
public_send(operator, to_arel_operand(right, table))
|
|
148
273
|
end
|
|
149
274
|
end
|
|
150
275
|
|
|
151
276
|
class Aggregate < Node
|
|
152
277
|
include Predications
|
|
278
|
+
include Arithmetics
|
|
153
279
|
|
|
154
|
-
attr_reader :operand, :function
|
|
280
|
+
attr_reader :operand, :function, :distinct
|
|
155
281
|
|
|
156
|
-
def initialize(operand, function)
|
|
282
|
+
def initialize(operand, function, distinct: false)
|
|
283
|
+
if distinct && function != :count
|
|
284
|
+
raise ArgumentError, "#{function} does not take distinct"
|
|
285
|
+
end
|
|
286
|
+
if distinct && operand == :*
|
|
287
|
+
raise ArgumentError, "count(:*) does not take distinct; name a column"
|
|
288
|
+
end
|
|
157
289
|
@operand = operand
|
|
158
290
|
@function = function
|
|
291
|
+
@distinct = distinct
|
|
159
292
|
end
|
|
160
293
|
|
|
161
294
|
def to_arel(table)
|
|
162
|
-
to_arel_operand(operand, table)
|
|
295
|
+
arel_operand = to_arel_operand(operand, table)
|
|
296
|
+
if function == :count
|
|
297
|
+
arel_operand.count(distinct)
|
|
298
|
+
else
|
|
299
|
+
arel_operand.public_send(function)
|
|
300
|
+
end
|
|
163
301
|
end
|
|
164
302
|
end
|
|
165
303
|
|
|
@@ -168,7 +306,7 @@ module ActiveRecord
|
|
|
168
306
|
|
|
169
307
|
def initialize(operand, alias_name)
|
|
170
308
|
@operand = operand
|
|
171
|
-
@alias_name = alias_name
|
|
309
|
+
@alias_name = AST.check_name(alias_name, ALIAS_NAME, "column alias")
|
|
172
310
|
end
|
|
173
311
|
|
|
174
312
|
def to_arel(table)
|
|
@@ -177,20 +315,33 @@ module ActiveRecord
|
|
|
177
315
|
end
|
|
178
316
|
|
|
179
317
|
class Ordering < Node
|
|
180
|
-
attr_reader :operand, :direction
|
|
318
|
+
attr_reader :operand, :direction, :nulls
|
|
181
319
|
|
|
182
|
-
def initialize(operand, direction)
|
|
320
|
+
def initialize(operand, direction, nulls = nil)
|
|
183
321
|
@operand = operand
|
|
184
322
|
@direction = direction
|
|
323
|
+
@nulls = nulls
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# MySQL has no NULLS FIRST/LAST, but Arel emulates it there with a
|
|
327
|
+
# leading IS NULL ordering, so these are portable.
|
|
328
|
+
def nulls_first
|
|
329
|
+
Ordering.new(operand, direction, :nulls_first)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def nulls_last
|
|
333
|
+
Ordering.new(operand, direction, :nulls_last)
|
|
185
334
|
end
|
|
186
335
|
|
|
187
336
|
def to_arel(table)
|
|
188
|
-
to_arel_operand(operand, table).public_send(direction)
|
|
337
|
+
ordering = to_arel_operand(operand, table).public_send(direction)
|
|
338
|
+
nulls ? ordering.public_send(nulls) : ordering
|
|
189
339
|
end
|
|
190
340
|
end
|
|
191
341
|
|
|
192
342
|
class Function < Node
|
|
193
343
|
include Predications
|
|
344
|
+
include Arithmetics
|
|
194
345
|
|
|
195
346
|
attr_reader :name, :args
|
|
196
347
|
|
|
@@ -229,9 +380,27 @@ module ActiveRecord
|
|
|
229
380
|
|
|
230
381
|
def to_arel(table)
|
|
231
382
|
arel_column = to_arel_operand(column, table)
|
|
232
|
-
arel_value =
|
|
383
|
+
arel_value =
|
|
384
|
+
case value
|
|
385
|
+
when Node then value.to_arel(table)
|
|
386
|
+
when ActiveRecord::Relation then scalar_subquery(value)
|
|
387
|
+
else value
|
|
388
|
+
end
|
|
233
389
|
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
234
390
|
end
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
# A relation compared against a column has to yield a single value, so
|
|
395
|
+
# unlike In there is no sensible default select list to fall back on.
|
|
396
|
+
def scalar_subquery(relation)
|
|
397
|
+
if relation.select_values.empty?
|
|
398
|
+
raise ArgumentError,
|
|
399
|
+
"#{operator} needs a subquery selecting one value; add a select"
|
|
400
|
+
end
|
|
401
|
+
relation = relation.send(:apply_join_dependency) if relation.eager_loading?
|
|
402
|
+
relation.arel
|
|
403
|
+
end
|
|
235
404
|
end
|
|
236
405
|
|
|
237
406
|
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
@@ -310,38 +479,80 @@ module ActiveRecord
|
|
|
310
479
|
inject {|left, right| Or.new(left, right) }
|
|
311
480
|
end
|
|
312
481
|
|
|
313
|
-
attr_reader :operand, :pattern, :escape
|
|
482
|
+
attr_reader :operand, :pattern, :escape, :case_sensitive
|
|
314
483
|
|
|
315
|
-
def initialize(operand, pattern, escape = nil)
|
|
484
|
+
def initialize(operand, pattern, escape = nil, case_sensitive: true)
|
|
316
485
|
@operand = operand
|
|
317
486
|
@pattern = pattern
|
|
318
487
|
@escape = escape
|
|
488
|
+
@case_sensitive = case_sensitive
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def to_arel(table)
|
|
492
|
+
# Arel matches case-insensitively unless told otherwise, which is
|
|
493
|
+
# what picks ILIKE over LIKE on PostgreSQL.
|
|
494
|
+
to_arel_operand(operand, table).matches(pattern, escape, case_sensitive)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# IS [NOT] DISTINCT FROM, spelled IS / IS NOT on SQLite and <=> on
|
|
499
|
+
# MySQL. NULL compares as a value here, which is what separates these
|
|
500
|
+
# from = and <>.
|
|
501
|
+
class DistinctFrom < Predicate
|
|
502
|
+
attr_reader :operand, :value, :negated
|
|
503
|
+
|
|
504
|
+
def initialize(operand, value, negated: false)
|
|
505
|
+
@operand = operand
|
|
506
|
+
@value = value
|
|
507
|
+
@negated = negated
|
|
319
508
|
end
|
|
320
509
|
|
|
321
510
|
def to_arel(table)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
511
|
+
arel_operand = to_arel_operand(operand, table)
|
|
512
|
+
arel_value = value.is_a?(Node) ? value.to_arel(table) : value
|
|
513
|
+
if negated
|
|
514
|
+
arel_operand.is_distinct_from(arel_value)
|
|
515
|
+
else
|
|
516
|
+
arel_operand.is_not_distinct_from(arel_value)
|
|
517
|
+
end
|
|
325
518
|
end
|
|
326
519
|
end
|
|
327
520
|
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
|
|
335
|
-
|
|
521
|
+
# Comparisons against a PostgreSQL array column, named after the Ruby
|
|
522
|
+
# methods that mean the same thing: member? is Enumerable's element
|
|
523
|
+
# test, superset? and subset? are Set's whole-array containment, and
|
|
524
|
+
# intersect? is Array's "any element in common". Each name maps to one
|
|
525
|
+
# operator; the elements are rendered as an array literal, which
|
|
526
|
+
# PostgreSQL coerces to the column's element type, so any expression
|
|
527
|
+
# works as the operand and no schema lookup is needed.
|
|
528
|
+
class ArrayPredicate < Predicate
|
|
529
|
+
# The whole-array comparisons take the collection kinds their
|
|
530
|
+
# namesakes compare against: an Array, or a Set for the Set methods.
|
|
531
|
+
def self.elements(arg, method_name)
|
|
532
|
+
case arg
|
|
533
|
+
when ::Array then arg
|
|
534
|
+
when ::Set then arg.to_a
|
|
535
|
+
else
|
|
536
|
+
raise ArgumentError, "#{method_name} takes an Array or Set of elements"
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
attr_reader :operand, :operator, :elements
|
|
336
541
|
|
|
337
|
-
def initialize(operand,
|
|
542
|
+
def initialize(operand, operator, elements)
|
|
338
543
|
@operand = operand
|
|
339
|
-
@
|
|
544
|
+
@operator = operator
|
|
545
|
+
@elements = elements
|
|
340
546
|
end
|
|
341
547
|
|
|
342
548
|
def to_arel(table)
|
|
343
549
|
arel_operand = to_arel_operand(operand, table)
|
|
344
|
-
|
|
550
|
+
quoted = Arel::Nodes.build_quoted(array_literal)
|
|
551
|
+
case operator
|
|
552
|
+
when :"@>" then Arel::Nodes::Contains.new(arel_operand, quoted)
|
|
553
|
+
when :"&&" then Arel::Nodes::Overlaps.new(arel_operand, quoted)
|
|
554
|
+
else Arel::Nodes::InfixOperation.new(operator, arel_operand, quoted)
|
|
555
|
+
end
|
|
345
556
|
end
|
|
346
557
|
|
|
347
558
|
private
|
|
@@ -3,10 +3,8 @@ module ActiveRecord
|
|
|
3
3
|
module BlockSyntax
|
|
4
4
|
refine Symbol do
|
|
5
5
|
import_methods AST::Predications
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
define_method(func) { AST::Aggregate.new(self, func) }
|
|
9
|
-
end
|
|
6
|
+
import_methods AST::Arithmetics
|
|
7
|
+
import_methods AST::Aggregations
|
|
10
8
|
|
|
11
9
|
def as(alias_name)
|
|
12
10
|
AST::As.new(self, alias_name)
|
|
@@ -27,23 +25,96 @@ module ActiveRecord
|
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
class BlockContext
|
|
28
|
+
# The model is only consulted to learn which adapter the query is being
|
|
29
|
+
# built for, which is what decides how a scalar function is spelled.
|
|
30
|
+
def initialize(model)
|
|
31
|
+
@model = model
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
AGGREGATE_FUNCTIONS = {
|
|
31
|
-
|
|
35
|
+
sum: :sum, avg: :average, min: :minimum, max: :maximum,
|
|
32
36
|
}.freeze
|
|
33
37
|
|
|
34
38
|
AGGREGATE_FUNCTIONS.each do |name, arel_func|
|
|
35
39
|
define_method(name) {|column| AST::Aggregate.new(column, arel_func) }
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
def count(column, distinct: false)
|
|
43
|
+
AST::Aggregate.new(column, :count, distinct: distinct)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Scalar functions, defined as real methods so that a typo is a
|
|
47
|
+
# NoMethodError and a name Kernel also answers to (format, hash, test)
|
|
48
|
+
# cannot quietly mean something else.
|
|
49
|
+
#
|
|
50
|
+
# The value lists the adapters that differ: a string is what the
|
|
51
|
+
# function is called there, nil says the adapter has no equivalent. An
|
|
52
|
+
# adapter that is not listed spells it like the method. The families
|
|
53
|
+
# are what the entries key on, so trilogy reads the mysql column.
|
|
54
|
+
#
|
|
55
|
+
# Availability was checked by calling each one; the SQLite figures
|
|
56
|
+
# assume the math functions its build usually enables.
|
|
57
|
+
SCALAR_FUNCTIONS = {
|
|
58
|
+
abs: {}, ceil: {}, coalesce: {}, concat: {}, exp: {}, floor: {},
|
|
59
|
+
length: {}, ln: {}, log: {}, lower: {}, ltrim: {}, mod: {},
|
|
60
|
+
nullif: {}, power: {}, replace: {}, round: {}, rtrim: {}, sqrt: {},
|
|
61
|
+
substr: {}, trim: {}, upper: {},
|
|
62
|
+
char_length: {sqlite: 'LENGTH'},
|
|
63
|
+
greatest: {sqlite: 'MAX'},
|
|
64
|
+
least: {sqlite: 'MIN'},
|
|
65
|
+
now: {sqlite: nil},
|
|
66
|
+
date_trunc: {sqlite: nil, mysql: nil},
|
|
67
|
+
# Named for Kernel#rand, which it also takes back: a block calling
|
|
68
|
+
# rand would otherwise get Ruby's and never reach the database.
|
|
69
|
+
rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
|
|
70
|
+
# Two different functions share this name: printf formatting here, and
|
|
71
|
+
# on MySQL the one that puts separators in a number, which reads a
|
|
72
|
+
# printf template as the number zero rather than complaining. The
|
|
73
|
+
# name keeps the one meaning; fn(:format, ...) reaches MySQL's.
|
|
74
|
+
format: {mysql: nil},
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
ADAPTER_FAMILIES = {
|
|
78
|
+
'sqlite3' => :sqlite,
|
|
79
|
+
'postgresql' => :postgresql,
|
|
80
|
+
'postgis' => :postgresql,
|
|
81
|
+
'mysql2' => :mysql,
|
|
82
|
+
'trilogy' => :mysql,
|
|
83
|
+
}.freeze
|
|
39
84
|
|
|
40
|
-
SCALAR_FUNCTIONS.
|
|
41
|
-
define_method(name) {|*args| AST::Function.new(name
|
|
85
|
+
SCALAR_FUNCTIONS.each_key do |name|
|
|
86
|
+
define_method(name) {|*args| AST::Function.new(function_name(name), args) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Escape hatch for functions without a method of their own. The name is
|
|
90
|
+
# emitted as written, so a case-sensitive one can be spelled exactly,
|
|
91
|
+
# and for that reason it has to be a plain name, optionally qualified by
|
|
92
|
+
# a schema; anything else is refused rather than written into the SQL.
|
|
93
|
+
def fn(name, *args)
|
|
94
|
+
AST::Function.new(
|
|
95
|
+
AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
|
|
42
96
|
end
|
|
43
97
|
|
|
44
98
|
def exists?(relation)
|
|
45
99
|
AST::Exists.new(relation)
|
|
46
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def function_name(name)
|
|
105
|
+
spellings = SCALAR_FUNCTIONS.fetch(name)
|
|
106
|
+
return name.to_s.upcase unless spellings.key?(adapter_family)
|
|
107
|
+
spellings.fetch(adapter_family) ||
|
|
108
|
+
raise(NotImplementedError,
|
|
109
|
+
"#{name} has no equivalent on #{@model.connection_db_config.adapter}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# An adapter nobody has classified keeps the standard spellings, and is
|
|
113
|
+
# left to say for itself what it cannot do.
|
|
114
|
+
def adapter_family
|
|
115
|
+
@adapter_family ||=
|
|
116
|
+
ADAPTER_FAMILIES[@model.connection_db_config.adapter] || :unknown
|
|
117
|
+
end
|
|
47
118
|
end
|
|
48
119
|
|
|
49
120
|
module QueryMethods
|
|
@@ -93,19 +164,39 @@ module ActiveRecord
|
|
|
93
164
|
end
|
|
94
165
|
end
|
|
95
166
|
|
|
96
|
-
|
|
167
|
+
# A symbol names a table, which ActiveRecord's own from only takes as a
|
|
168
|
+
# string. With `as` it is selected under another name, which is how a
|
|
169
|
+
# CTE stands in for the model's own table:
|
|
170
|
+
# with_recursive(tree: [...]).from(:tree, as: :nodes)
|
|
171
|
+
def from(value, subquery_name = nil, as: nil)
|
|
172
|
+
unless value.is_a?(Symbol)
|
|
173
|
+
if as
|
|
174
|
+
raise ArgumentError, "as: needs the table named as a symbol"
|
|
175
|
+
end
|
|
176
|
+
return super(value, subquery_name)
|
|
177
|
+
end
|
|
178
|
+
arel_table = Arel::Table.new(value)
|
|
179
|
+
arel_table = arel_table.alias(as) if as
|
|
180
|
+
super(arel_table, subquery_name)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# `as` names the table within the query, which is what makes a self
|
|
184
|
+
# join expressible: joins(:employees, as: :managers) { ... }.
|
|
185
|
+
def joins(*args, as: nil, &block)
|
|
97
186
|
if block
|
|
98
|
-
super(build_join_node(args.first, Arel::Nodes::InnerJoin, &block))
|
|
187
|
+
super(build_join_node(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
99
188
|
else
|
|
100
|
-
|
|
189
|
+
reject_join_alias(as)
|
|
190
|
+
super(*args, &block)
|
|
101
191
|
end
|
|
102
192
|
end
|
|
103
193
|
|
|
104
|
-
def left_outer_joins(*args, &block)
|
|
194
|
+
def left_outer_joins(*args, as: nil, &block)
|
|
105
195
|
if block
|
|
106
|
-
joins(build_join_node(args.first, Arel::Nodes::OuterJoin, &block))
|
|
196
|
+
joins(build_join_node(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
107
197
|
else
|
|
108
|
-
|
|
198
|
+
reject_join_alias(as)
|
|
199
|
+
super(*args, &block)
|
|
109
200
|
end
|
|
110
201
|
end
|
|
111
202
|
|
|
@@ -113,7 +204,7 @@ module ActiveRecord
|
|
|
113
204
|
|
|
114
205
|
def evaluate_block(&block)
|
|
115
206
|
refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
|
|
116
|
-
BlockContext.new.instance_exec(&refined_block)
|
|
207
|
+
BlockContext.new(klass).instance_exec(&refined_block)
|
|
117
208
|
end
|
|
118
209
|
|
|
119
210
|
def to_arel_field(node)
|
|
@@ -124,12 +215,16 @@ module ActiveRecord
|
|
|
124
215
|
end
|
|
125
216
|
end
|
|
126
217
|
|
|
127
|
-
def
|
|
218
|
+
def reject_join_alias(alias_name)
|
|
219
|
+
return unless alias_name
|
|
220
|
+
raise ArgumentError, "as: needs a block to write the ON clause with"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_join_node(target_table, join_class, alias_name, &block)
|
|
128
224
|
ast = evaluate_block(&block)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
225
|
+
arel_table = Arel::Table.new(target_table)
|
|
226
|
+
arel_table = arel_table.alias(alias_name) if alias_name
|
|
227
|
+
join_class.new(arel_table, Arel::Nodes::On.new(ast.to_arel(table)))
|
|
133
228
|
end
|
|
134
229
|
end
|
|
135
230
|
end
|