activerecord-refined 0.3.2 → 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 +260 -21
- 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 +335 -20
- data/lib/active_record/refined.rb +119 -20
- data/lib/activerecord-refined/version.rb +1 -1
- data/test/test_block_syntax.rb +579 -0
- data/test/test_helper.rb +21 -1
- metadata +7 -1
|
@@ -1,15 +1,38 @@
|
|
|
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.
|
|
7
21
|
module Predications
|
|
22
|
+
# == and != mean SQL = and <>, and = NULL is never true there, so nil
|
|
23
|
+
# is rejected rather than silently rewritten to IS NULL. null? builds
|
|
24
|
+
# its node directly and stays clear of this check.
|
|
8
25
|
def ==(other)
|
|
26
|
+
if other.nil?
|
|
27
|
+
raise ArgumentError, "== does not take nil; use null? instead"
|
|
28
|
+
end
|
|
9
29
|
Comparison.new(self, :==, other)
|
|
10
30
|
end
|
|
11
31
|
|
|
12
32
|
def !=(other)
|
|
33
|
+
if other.nil?
|
|
34
|
+
raise ArgumentError, "!= does not take nil; use !null? instead"
|
|
35
|
+
end
|
|
13
36
|
Comparison.new(self, :!=, other)
|
|
14
37
|
end
|
|
15
38
|
|
|
@@ -53,17 +76,118 @@ module ActiveRecord
|
|
|
53
76
|
Like.new(self, pattern)
|
|
54
77
|
end
|
|
55
78
|
|
|
56
|
-
def
|
|
57
|
-
Like.new(self,
|
|
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
|
+
|
|
103
|
+
def start_with?(*prefixes)
|
|
104
|
+
if prefixes.empty?
|
|
105
|
+
raise ArgumentError, "start_with? needs at least one prefix"
|
|
106
|
+
end
|
|
107
|
+
Like.any(self, prefixes.map {|prefix| "#{Like.escape(prefix)}%" })
|
|
58
108
|
end
|
|
59
109
|
|
|
60
|
-
def end_with?(
|
|
61
|
-
|
|
110
|
+
def end_with?(*suffixes)
|
|
111
|
+
if suffixes.empty?
|
|
112
|
+
raise ArgumentError, "end_with? needs at least one suffix"
|
|
113
|
+
end
|
|
114
|
+
Like.any(self, suffixes.map {|suffix| "%#{Like.escape(suffix)}" })
|
|
62
115
|
end
|
|
63
116
|
|
|
64
117
|
def include?(substring)
|
|
65
118
|
Like.new(self, "%#{Like.escape(substring)}%", Like::ESCAPE)
|
|
66
119
|
end
|
|
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.
|
|
125
|
+
def member?(element)
|
|
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)
|
|
190
|
+
end
|
|
67
191
|
end
|
|
68
192
|
|
|
69
193
|
class Node
|
|
@@ -112,6 +236,8 @@ module ActiveRecord
|
|
|
112
236
|
|
|
113
237
|
class Column < Node
|
|
114
238
|
include Predications
|
|
239
|
+
include Arithmetics
|
|
240
|
+
include Aggregations
|
|
115
241
|
|
|
116
242
|
attr_reader :table_name, :column_name
|
|
117
243
|
|
|
@@ -123,24 +249,55 @@ module ActiveRecord
|
|
|
123
249
|
def to_arel(_table)
|
|
124
250
|
Arel::Table.new(table_name)[column_name]
|
|
125
251
|
end
|
|
252
|
+
end
|
|
253
|
+
|
|
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
|
|
126
261
|
|
|
127
|
-
|
|
128
|
-
|
|
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))
|
|
129
273
|
end
|
|
130
274
|
end
|
|
131
275
|
|
|
132
276
|
class Aggregate < Node
|
|
133
277
|
include Predications
|
|
278
|
+
include Arithmetics
|
|
134
279
|
|
|
135
|
-
attr_reader :operand, :function
|
|
280
|
+
attr_reader :operand, :function, :distinct
|
|
136
281
|
|
|
137
|
-
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
|
|
138
289
|
@operand = operand
|
|
139
290
|
@function = function
|
|
291
|
+
@distinct = distinct
|
|
140
292
|
end
|
|
141
293
|
|
|
142
294
|
def to_arel(table)
|
|
143
|
-
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
|
|
144
301
|
end
|
|
145
302
|
end
|
|
146
303
|
|
|
@@ -149,7 +306,7 @@ module ActiveRecord
|
|
|
149
306
|
|
|
150
307
|
def initialize(operand, alias_name)
|
|
151
308
|
@operand = operand
|
|
152
|
-
@alias_name = alias_name
|
|
309
|
+
@alias_name = AST.check_name(alias_name, ALIAS_NAME, "column alias")
|
|
153
310
|
end
|
|
154
311
|
|
|
155
312
|
def to_arel(table)
|
|
@@ -158,20 +315,33 @@ module ActiveRecord
|
|
|
158
315
|
end
|
|
159
316
|
|
|
160
317
|
class Ordering < Node
|
|
161
|
-
attr_reader :operand, :direction
|
|
318
|
+
attr_reader :operand, :direction, :nulls
|
|
162
319
|
|
|
163
|
-
def initialize(operand, direction)
|
|
320
|
+
def initialize(operand, direction, nulls = nil)
|
|
164
321
|
@operand = operand
|
|
165
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)
|
|
166
334
|
end
|
|
167
335
|
|
|
168
336
|
def to_arel(table)
|
|
169
|
-
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
|
|
170
339
|
end
|
|
171
340
|
end
|
|
172
341
|
|
|
173
342
|
class Function < Node
|
|
174
343
|
include Predications
|
|
344
|
+
include Arithmetics
|
|
175
345
|
|
|
176
346
|
attr_reader :name, :args
|
|
177
347
|
|
|
@@ -210,12 +380,31 @@ module ActiveRecord
|
|
|
210
380
|
|
|
211
381
|
def to_arel(table)
|
|
212
382
|
arel_column = to_arel_operand(column, table)
|
|
213
|
-
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
|
|
214
389
|
arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
|
|
215
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
|
|
216
404
|
end
|
|
217
405
|
|
|
218
|
-
# IN for a list of values, BETWEEN for a range
|
|
406
|
+
# IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
|
|
407
|
+
# relation.
|
|
219
408
|
class In < Predicate
|
|
220
409
|
attr_reader :operand, :values
|
|
221
410
|
|
|
@@ -228,9 +417,49 @@ module ActiveRecord
|
|
|
228
417
|
arel_operand = to_arel_operand(operand, table)
|
|
229
418
|
case values
|
|
230
419
|
when Range then arel_operand.between(values)
|
|
420
|
+
when ActiveRecord::Relation then arel_operand.in(subquery(values))
|
|
231
421
|
else arel_operand.in(values)
|
|
232
422
|
end
|
|
233
423
|
end
|
|
424
|
+
|
|
425
|
+
private
|
|
426
|
+
|
|
427
|
+
# The same treatment ActiveRecord's own RelationHandler gives a
|
|
428
|
+
# relation used as a value: without an explicit select list the
|
|
429
|
+
# subquery selects the model's primary key.
|
|
430
|
+
def subquery(relation)
|
|
431
|
+
if relation.eager_loading?
|
|
432
|
+
relation = relation.send(:apply_join_dependency)
|
|
433
|
+
end
|
|
434
|
+
if relation.select_values.empty?
|
|
435
|
+
model = relation.model
|
|
436
|
+
if model.composite_primary_key?
|
|
437
|
+
raise ArgumentError,
|
|
438
|
+
"Cannot map composite primary key #{model.primary_key} to IN"
|
|
439
|
+
end
|
|
440
|
+
relation = relation.select(relation.table[model.primary_key])
|
|
441
|
+
end
|
|
442
|
+
relation.arel
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# EXISTS (SELECT ...) for a relation. Correlate the subquery with the
|
|
447
|
+
# outer table through qualified columns. EXISTS only asks whether a row
|
|
448
|
+
# comes back, so unlike In there is no select list to fix up.
|
|
449
|
+
class Exists < Predicate
|
|
450
|
+
attr_reader :relation
|
|
451
|
+
|
|
452
|
+
def initialize(relation)
|
|
453
|
+
@relation = relation
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def to_arel(_table)
|
|
457
|
+
subquery = relation
|
|
458
|
+
if subquery.eager_loading?
|
|
459
|
+
subquery = subquery.send(:apply_join_dependency)
|
|
460
|
+
end
|
|
461
|
+
subquery.arel.exists
|
|
462
|
+
end
|
|
234
463
|
end
|
|
235
464
|
|
|
236
465
|
class Like < Predicate
|
|
@@ -243,18 +472,104 @@ module ActiveRecord
|
|
|
243
472
|
ActiveRecord::Base.sanitize_sql_like(string, ESCAPE)
|
|
244
473
|
end
|
|
245
474
|
|
|
246
|
-
|
|
475
|
+
# ORs one LIKE per pattern, for the shortcuts that accept several
|
|
476
|
+
# literals the way String#start_with? does.
|
|
477
|
+
def self.any(operand, patterns)
|
|
478
|
+
patterns.map {|pattern| new(operand, pattern, ESCAPE) }.
|
|
479
|
+
inject {|left, right| Or.new(left, right) }
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
attr_reader :operand, :pattern, :escape, :case_sensitive
|
|
247
483
|
|
|
248
|
-
def initialize(operand, pattern, escape = nil)
|
|
484
|
+
def initialize(operand, pattern, escape = nil, case_sensitive: true)
|
|
249
485
|
@operand = operand
|
|
250
486
|
@pattern = pattern
|
|
251
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
|
|
252
508
|
end
|
|
253
509
|
|
|
254
510
|
def to_arel(table)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
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
|
|
541
|
+
|
|
542
|
+
def initialize(operand, operator, elements)
|
|
543
|
+
@operand = operand
|
|
544
|
+
@operator = operator
|
|
545
|
+
@elements = elements
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def to_arel(table)
|
|
549
|
+
arel_operand = to_arel_operand(operand, table)
|
|
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
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
private
|
|
559
|
+
|
|
560
|
+
# PostgreSQL array input syntax: elements joined by commas inside
|
|
561
|
+
# braces, and an element is double-quoted whenever it is empty, spells
|
|
562
|
+
# NULL, or contains a character the parser treats specially.
|
|
563
|
+
def array_literal
|
|
564
|
+
encoded = elements.map do |value|
|
|
565
|
+
s = value.to_s
|
|
566
|
+
if s.empty? || s.casecmp?("null") || s.match?(/[\s{},"\\]/)
|
|
567
|
+
"\"#{s.gsub(/["\\]/) {|c| "\\#{c}" }}\""
|
|
568
|
+
else
|
|
569
|
+
s
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
"{#{encoded.join(',')}}"
|
|
258
573
|
end
|
|
259
574
|
end
|
|
260
575
|
|
|
@@ -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,18 +25,95 @@ 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
|
|
84
|
+
|
|
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)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def exists?(relation)
|
|
99
|
+
AST::Exists.new(relation)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
39
103
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
117
|
end
|
|
43
118
|
end
|
|
44
119
|
|
|
@@ -89,19 +164,39 @@ module ActiveRecord
|
|
|
89
164
|
end
|
|
90
165
|
end
|
|
91
166
|
|
|
92
|
-
|
|
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)
|
|
93
186
|
if block
|
|
94
|
-
super(build_join_node(args.first, Arel::Nodes::InnerJoin, &block))
|
|
187
|
+
super(build_join_node(args.first, Arel::Nodes::InnerJoin, as, &block))
|
|
95
188
|
else
|
|
96
|
-
|
|
189
|
+
reject_join_alias(as)
|
|
190
|
+
super(*args, &block)
|
|
97
191
|
end
|
|
98
192
|
end
|
|
99
193
|
|
|
100
|
-
def left_outer_joins(*args, &block)
|
|
194
|
+
def left_outer_joins(*args, as: nil, &block)
|
|
101
195
|
if block
|
|
102
|
-
joins(build_join_node(args.first, Arel::Nodes::OuterJoin, &block))
|
|
196
|
+
joins(build_join_node(args.first, Arel::Nodes::OuterJoin, as, &block))
|
|
103
197
|
else
|
|
104
|
-
|
|
198
|
+
reject_join_alias(as)
|
|
199
|
+
super(*args, &block)
|
|
105
200
|
end
|
|
106
201
|
end
|
|
107
202
|
|
|
@@ -109,7 +204,7 @@ module ActiveRecord
|
|
|
109
204
|
|
|
110
205
|
def evaluate_block(&block)
|
|
111
206
|
refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
|
|
112
|
-
BlockContext.new.instance_exec(&refined_block)
|
|
207
|
+
BlockContext.new(klass).instance_exec(&refined_block)
|
|
113
208
|
end
|
|
114
209
|
|
|
115
210
|
def to_arel_field(node)
|
|
@@ -120,12 +215,16 @@ module ActiveRecord
|
|
|
120
215
|
end
|
|
121
216
|
end
|
|
122
217
|
|
|
123
|
-
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)
|
|
124
224
|
ast = evaluate_block(&block)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
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)))
|
|
129
228
|
end
|
|
130
229
|
end
|
|
131
230
|
end
|