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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +14 -0
- data/.rubocop.yml +393 -0
- data/Gemfile +5 -3
- data/README.md +254 -74
- data/Rakefile +31 -3
- data/activerecord-refined.gemspec +29 -15
- data/benchmark/query_building.rb +11 -11
- data/examples/aggregations.rb +13 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +79 -44
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +862 -272
- data/lib/active_record/refined.rb +260 -180
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +8 -5
- data/test/test_block_syntax.rb +879 -323
- data/test/test_helper.rb +56 -39
- metadata +130 -1
|
@@ -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
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
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:
|
|
76
|
-
greatest: {sqlite:
|
|
77
|
-
least: {sqlite:
|
|
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:
|
|
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:
|
|
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
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
# PostgreSQL alone, so what it would raise
|
|
157
|
-
# says it here, as extract does, while
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
259
|
+
when :mysql then AST::Function.new("BIT_COUNT", [expr])
|
|
222
260
|
when :postgresql
|
|
223
|
-
AST::Function.new(
|
|
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(
|
|
281
|
+
quantified("ANY", relation)
|
|
244
282
|
end
|
|
245
283
|
|
|
246
284
|
def all(relation)
|
|
247
|
-
quantified(
|
|
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
|
-
#
|
|
255
|
-
#
|
|
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(
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
super(*
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
"
|
|
605
|
+
"WITH ROLLUP takes the whole group list; group by the rollup alone"
|
|
576
606
|
end
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
end
|
|
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
|