activerecord-refined 0.10.2 → 0.12.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/.yardopts +1 -1
- data/README.md +19 -22
- data/activerecord-refined.gemspec +4 -3
- data/docs/conditions.md +3 -1
- data/docs/ctes.md +2 -2
- data/docs/expressions.md +37 -20
- data/docs/functions.md +14 -10
- data/docs/index.md +2 -17
- data/docs/windows.md +2 -2
- data/examples/expressions.rb +21 -15
- data/lib/active_record/refined/ast/arithmetics.rb +137 -0
- data/lib/active_record/refined/ast/conditions.rb +451 -0
- data/lib/active_record/refined/ast/expressions.rb +304 -0
- data/lib/active_record/refined/ast/functions.rb +254 -0
- data/lib/active_record/refined/ast/grouping.rb +63 -0
- data/lib/active_record/refined/ast/json.rb +530 -0
- data/lib/active_record/refined/ast/node.rb +298 -0
- data/lib/active_record/refined/ast/ordering.rb +104 -0
- data/lib/active_record/refined/ast/predications.rb +384 -0
- data/lib/active_record/refined/ast/windows.rb +126 -0
- data/lib/active_record/refined/ast.rb +39 -2364
- data/lib/active_record/refined/block_context.rb +671 -0
- data/lib/active_record/refined/block_syntax.rb +128 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
- data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
- data/lib/active_record/refined/dialect/oracle.rb +1 -7
- data/lib/active_record/refined/dialect/postgresql.rb +8 -0
- data/lib/active_record/refined/dialect/sql_server.rb +29 -5
- data/lib/active_record/refined/dialect/sqlite.rb +23 -0
- data/lib/active_record/refined/dialect.rb +132 -66
- data/lib/active_record/refined/query_methods.rb +444 -0
- data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
- data/lib/active_record/refined/writes.rb +72 -0
- data/lib/active_record/refined.rb +7 -1270
- data/lib/activerecord-refined.rb +0 -4
- metadata +36 -5
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/node"
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Refined
|
|
7
|
+
module AST
|
|
8
|
+
# CASE, in both of the shapes SQL has for it. With an operand, each
|
|
9
|
+
# `when` is something to compare it against; without one, each `when` is
|
|
10
|
+
# a condition of its own.
|
|
11
|
+
#
|
|
12
|
+
# Every method returns a new node rather than adding to this one, so a
|
|
13
|
+
# case kept in a variable can be branched from more than once.
|
|
14
|
+
class Case < Node
|
|
15
|
+
include Predications
|
|
16
|
+
include Arithmetics
|
|
17
|
+
|
|
18
|
+
# Having no ELSE is not the same as an ELSE of nil, and nil is what an
|
|
19
|
+
# omitted argument looks like, so the absence needs a value of its own.
|
|
20
|
+
NOTHING = Object.new.freeze
|
|
21
|
+
private_constant :NOTHING
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
attr_reader :operand, :whens, :default
|
|
25
|
+
|
|
26
|
+
def initialize(operand = nil, whens = [], default = NOTHING)
|
|
27
|
+
@operand = operand
|
|
28
|
+
@whens = whens
|
|
29
|
+
@default = default
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The next `WHEN`: a value to compare the operand against, or a condition as a value or a block.
|
|
33
|
+
# @return [AST::Case::When]
|
|
34
|
+
def when(value = nil, &block)
|
|
35
|
+
When.new(self, Case.argument(:when, value, block))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `THEN`, which belongs after a `when`; here it says so.
|
|
39
|
+
# @raise [ArgumentError]
|
|
40
|
+
#
|
|
41
|
+
# Kernel#then is on every object, so `then` in the wrong place would be
|
|
42
|
+
# answered by it -- with no block, silently, with an Enumerator.
|
|
43
|
+
def then(*)
|
|
44
|
+
raise ArgumentError, "then follows a when, and there is none to follow here"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# `ELSE value`, as a value or a block, closing the CASE. Without one the CASE gives NULL where no `when` matched.
|
|
48
|
+
# @return [AST::Case]
|
|
49
|
+
def else(value = nil, &block)
|
|
50
|
+
Case.new(operand, whens, Case.argument(:else, value, block))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @private
|
|
54
|
+
def to_arel(table, model)
|
|
55
|
+
raise ArgumentError, "case needs a when before it means anything" if whens.empty?
|
|
56
|
+
|
|
57
|
+
node = operand ? Arel::Nodes::Case.new(to_arel_operand(operand, table, model))
|
|
58
|
+
: Arel::Nodes::Case.new
|
|
59
|
+
whens.each do |condition, result|
|
|
60
|
+
node.when(to_arel_argument(condition, table, model)).
|
|
61
|
+
then(to_arel_argument(result, table, model))
|
|
62
|
+
end
|
|
63
|
+
node.else(to_arel_argument(default, table, model)) unless default.equal?(NOTHING)
|
|
64
|
+
node
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A value or a block, and exactly one of them: the block is what makes
|
|
68
|
+
# `when { :age >= 60 }` read like the blocks around it, and the value is
|
|
69
|
+
# what makes `when(10)` possible at all.
|
|
70
|
+
# @private
|
|
71
|
+
def self.argument(name, value, block)
|
|
72
|
+
if block
|
|
73
|
+
raise ArgumentError, "#{name} takes a value or a block, not both" unless value.nil?
|
|
74
|
+
return block.call
|
|
75
|
+
end
|
|
76
|
+
raise ArgumentError, "#{name} needs a value or a block" if value.nil?
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# What a `when` is until its `then` arrives. A Node so that using it
|
|
81
|
+
# as one says what is missing rather than reaching Active Record as
|
|
82
|
+
# something it cannot read.
|
|
83
|
+
class When < Node
|
|
84
|
+
def initialize(kase, condition)
|
|
85
|
+
@kase = kase
|
|
86
|
+
@condition = condition
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# `THEN value`, as a value or a block, for the `when` before it.
|
|
90
|
+
# @return [AST::Case]
|
|
91
|
+
def then(value = nil, &block)
|
|
92
|
+
Case.new(@kase.operand,
|
|
93
|
+
@kase.whens + [[@condition, Case.argument(:then, value, block)]],
|
|
94
|
+
@kase.default)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @private
|
|
98
|
+
def to_arel(_table, _model)
|
|
99
|
+
raise ArgumentError, "when needs a matching then"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Arithmetic on columns and expressions. Ruby's precedence puts these
|
|
105
|
+
# above the comparison operators, so :price * :quantity > 100 groups the
|
|
106
|
+
# way it reads.
|
|
107
|
+
#
|
|
108
|
+
# A Duration on the right moves a date: `:due_on + 3.days`. No two
|
|
109
|
+
# families spell the move alike, so the dialect writes it, a part of the
|
|
110
|
+
# duration at a time.
|
|
111
|
+
class Arithmetic < Node
|
|
112
|
+
include Predications
|
|
113
|
+
include Arithmetics
|
|
114
|
+
|
|
115
|
+
# Active Support's parts, as the units the SQL takes. A week is seven
|
|
116
|
+
# days: SQLite and Oracle have no week.
|
|
117
|
+
# @private
|
|
118
|
+
UNITS = {
|
|
119
|
+
years: :year, months: :month, weeks: :day, days: :day,
|
|
120
|
+
hours: :hour, minutes: :minute, seconds: :second,
|
|
121
|
+
}.freeze
|
|
122
|
+
|
|
123
|
+
# @private
|
|
124
|
+
DATE_UNITS = %i[year month day].freeze
|
|
125
|
+
|
|
126
|
+
# @private
|
|
127
|
+
attr_reader :left, :operator, :right
|
|
128
|
+
|
|
129
|
+
def initialize(left, operator, right)
|
|
130
|
+
@left = left
|
|
131
|
+
@operator = operator
|
|
132
|
+
@right = right
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @private
|
|
136
|
+
def to_arel(table, model)
|
|
137
|
+
arel_left = to_arel_operand(left, table, model)
|
|
138
|
+
return move_date(arel_left, model) if right.is_a?(::ActiveSupport::Duration)
|
|
139
|
+
# The operator dispatches Arel's Math, which a bare number carries
|
|
140
|
+
# none of; quoted, it is a node with the same methods.
|
|
141
|
+
arel_left = Arel::Nodes.build_quoted(arel_left) if arel_left.is_a?(::Numeric)
|
|
142
|
+
arel_left.public_send(operator, to_arel_operand(right, table, model))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# SQLite has no date type, and its datetime() gives whatever it is
|
|
146
|
+
# handed a time of day, so a date column moved by a day would come
|
|
147
|
+
# back a midnight and sort past the same day written bare. Its
|
|
148
|
+
# dialect has date() for what is a date to begin with, and this is
|
|
149
|
+
# what says so: a column the model declares a date, CURRENT_DATE, or
|
|
150
|
+
# one of those already moved by a date's units. The other families
|
|
151
|
+
# keep the type themselves and never ask.
|
|
152
|
+
# @private
|
|
153
|
+
def self.date_operand?(operand, model)
|
|
154
|
+
case operand
|
|
155
|
+
when ::Symbol then model.type_for_attribute(operand).type == :date
|
|
156
|
+
when DatetimeValueFunction then operand.name == "CURRENT_DATE"
|
|
157
|
+
when Arithmetic
|
|
158
|
+
operand.right.is_a?(::ActiveSupport::Duration) &&
|
|
159
|
+
date_operand?(operand.left, model) &&
|
|
160
|
+
operand.right.parts.keys.all? { |part| DATE_UNITS.include?(UNITS[part]) }
|
|
161
|
+
else false
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
# Each amount is written into the SQL as a number, and a fraction
|
|
167
|
+
# of a unit is not one every family takes, so it has to be a whole
|
|
168
|
+
# one.
|
|
169
|
+
def move_date(date, model)
|
|
170
|
+
unless operator == :+ || operator == :-
|
|
171
|
+
raise ArgumentError,
|
|
172
|
+
"a duration is added to a date or subtracted from it, not #{operator}"
|
|
173
|
+
end
|
|
174
|
+
dialect = Dialect.for(model)
|
|
175
|
+
date_only = Arithmetic.date_operand?(left, model)
|
|
176
|
+
right.parts.reduce(date) do |arel, (part, amount)|
|
|
177
|
+
unless amount.is_a?(::Integer)
|
|
178
|
+
raise ArgumentError, "#{amount.inspect} #{part} is not a whole number of them"
|
|
179
|
+
end
|
|
180
|
+
unit = UNITS.fetch(part)
|
|
181
|
+
amount *= 7 if part == :weeks
|
|
182
|
+
dialect.add_interval(arel, amount, unit, operator == :-,
|
|
183
|
+
date_only && DATE_UNITS.include?(unit))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# What the bitwise operations refuse. Both refusals are there because
|
|
189
|
+
# the same Ruby would otherwise mean different things per adapter: MySQL
|
|
190
|
+
# and SQLite take a boolean for the one bit it is stored as, so
|
|
191
|
+
# `published.bitwise_and(active)` would quietly be the AND it looks
|
|
192
|
+
# like, while PostgreSQL has no such operator and would say so.
|
|
193
|
+
# @private
|
|
194
|
+
module BitwiseOperands
|
|
195
|
+
private
|
|
196
|
+
# Oracle is the family with no bitwise operators at all -- BITAND
|
|
197
|
+
# is a function, with no OR, XOR, shift or NOT beside it -- so the
|
|
198
|
+
# node refuses there before its server has to.
|
|
199
|
+
def check_operators_exist(model)
|
|
200
|
+
return if Dialect.for(model).bitwise_operators_supported?
|
|
201
|
+
raise NotImplementedError,
|
|
202
|
+
"the bitwise operations have no equivalent on " \
|
|
203
|
+
"#{model.connection_db_config.adapter}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# A predicate alone is refused. The escape hatches are not: what
|
|
207
|
+
# sql() or op() holds is as often an expression as a condition, and
|
|
208
|
+
# here it is being read as the former.
|
|
209
|
+
def check_operand(operand)
|
|
210
|
+
return operand unless operand.is_a?(Predicate)
|
|
211
|
+
raise ArgumentError,
|
|
212
|
+
"a condition cannot be an operand of a bitwise operation; " \
|
|
213
|
+
"& and | between conditions are AND and OR"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Only the unqualified column can be checked, since that is the one
|
|
217
|
+
# the model is known to have.
|
|
218
|
+
def check_not_boolean(operand, model)
|
|
219
|
+
return unless operand.is_a?(::Symbol)
|
|
220
|
+
return unless model.type_for_attribute(operand).type == :boolean
|
|
221
|
+
raise ArgumentError,
|
|
222
|
+
"#{operand.inspect} is a boolean column, which the bitwise " \
|
|
223
|
+
"operations do not take; #{operand.inspect}.true? is the condition"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# SQL's bitwise operations. Each parenthesises itself, which is what
|
|
228
|
+
# keeps the grouping the Ruby asked for: PostgreSQL gives & and | the
|
|
229
|
+
# same precedence and reads a | b & c from the left, where
|
|
230
|
+
# a.bitwise_or(b.bitwise_and(c)) means the other thing.
|
|
231
|
+
class Bitwise < Node
|
|
232
|
+
include Predications
|
|
233
|
+
include Arithmetics
|
|
234
|
+
include BitwiseOperands
|
|
235
|
+
|
|
236
|
+
# @private
|
|
237
|
+
NODES = {
|
|
238
|
+
:& => Arel::Nodes::BitwiseAnd,
|
|
239
|
+
:| => Arel::Nodes::BitwiseOr,
|
|
240
|
+
:<< => Arel::Nodes::BitwiseShiftLeft,
|
|
241
|
+
:>> => Arel::Nodes::BitwiseShiftRight,
|
|
242
|
+
}.freeze
|
|
243
|
+
|
|
244
|
+
# @private
|
|
245
|
+
attr_reader :left, :operator, :right
|
|
246
|
+
|
|
247
|
+
def initialize(left, operator, right)
|
|
248
|
+
@left = left
|
|
249
|
+
@operator = operator
|
|
250
|
+
@right = check_operand(right)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @private
|
|
254
|
+
def to_arel(table, model)
|
|
255
|
+
check_operators_exist(model)
|
|
256
|
+
check_not_boolean(left, model)
|
|
257
|
+
check_not_boolean(right, model)
|
|
258
|
+
arel_left = to_arel_operand(left, table, model)
|
|
259
|
+
arel_right = to_arel_argument(right, table, model)
|
|
260
|
+
Arel::Nodes::Grouping.new(
|
|
261
|
+
if operator == :^
|
|
262
|
+
xor(arel_left, arel_right, model)
|
|
263
|
+
else
|
|
264
|
+
NODES.fetch(operator).new(arel_left, arel_right)
|
|
265
|
+
end)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
# Arel has a node for XOR, but it writes ^ on every adapter, and ^ is
|
|
270
|
+
# exponentiation to PostgreSQL -- a wrong answer rather than an error.
|
|
271
|
+
# PostgreSQL's own spelling, #, is where a comment starts on MySQL, so
|
|
272
|
+
# it cannot be the portable one either. SQLite has no XOR at all;
|
|
273
|
+
# (a | b) - (a & b) is it, at the cost of naming each operand twice.
|
|
274
|
+
def xor(left, right, model)
|
|
275
|
+
Dialect.for(model).bitwise_xor(left, right)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# ~, which every adapter has. MySQL answers with the unsigned 64-bit
|
|
280
|
+
# number where the others answer with a negative one; the bits are the
|
|
281
|
+
# same, and only reading the value back tells them apart.
|
|
282
|
+
class BitwiseNot < Node
|
|
283
|
+
include Predications
|
|
284
|
+
include Arithmetics
|
|
285
|
+
include BitwiseOperands
|
|
286
|
+
|
|
287
|
+
# @private
|
|
288
|
+
attr_reader :operand
|
|
289
|
+
|
|
290
|
+
def initialize(operand)
|
|
291
|
+
@operand = check_operand(operand)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @private
|
|
295
|
+
def to_arel(table, model)
|
|
296
|
+
check_operators_exist(model)
|
|
297
|
+
check_not_boolean(operand, model)
|
|
298
|
+
Arel::Nodes::Grouping.new(
|
|
299
|
+
Arel::Nodes::BitwiseNot.new(to_arel_operand(operand, table, model)))
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/node"
|
|
4
|
+
require "active_record/refined/ast/windows"
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module Refined
|
|
8
|
+
module AST
|
|
9
|
+
# An aggregate -- `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` -- over a group,
|
|
10
|
+
# or, given {Windowing#over}, a window.
|
|
11
|
+
class Aggregate < Node
|
|
12
|
+
include Predications
|
|
13
|
+
include Arithmetics
|
|
14
|
+
include Windowing
|
|
15
|
+
|
|
16
|
+
# The aggregates DISTINCT changes: over each value once, count counts
|
|
17
|
+
# fewer and sum and avg reckon less. min and max give the same
|
|
18
|
+
# either way, so a DISTINCT there is refused as saying nothing.
|
|
19
|
+
# @private
|
|
20
|
+
DISTINCT_FUNCTIONS = %i[count sum average].freeze
|
|
21
|
+
|
|
22
|
+
# @private
|
|
23
|
+
attr_reader :operand, :function, :distinct, :condition
|
|
24
|
+
|
|
25
|
+
def initialize(operand, function, distinct: false, condition: nil)
|
|
26
|
+
if distinct && !DISTINCT_FUNCTIONS.include?(function)
|
|
27
|
+
raise ArgumentError, "#{function} does not take distinct; it would give the same"
|
|
28
|
+
end
|
|
29
|
+
if distinct && operand == :*
|
|
30
|
+
raise ArgumentError, "count(:*) does not take distinct; name a column"
|
|
31
|
+
end
|
|
32
|
+
@operand = operand
|
|
33
|
+
@function = function
|
|
34
|
+
@distinct = distinct
|
|
35
|
+
@condition = condition
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `FILTER (WHERE condition)`: the aggregate taken over the rows the
|
|
39
|
+
# condition holds for, as a value or a block. Where there is no
|
|
40
|
+
# FILTER clause -- MySQL, SQL Server -- the CASE that means the same.
|
|
41
|
+
# @return [AST::Aggregate]
|
|
42
|
+
# @example
|
|
43
|
+
# Author.select { [count(:*).as(:all), count(:*).filter { :age < 50 }.as(:young)] }
|
|
44
|
+
def filter(condition = nil, &block)
|
|
45
|
+
Aggregate.new(operand, function, distinct: distinct,
|
|
46
|
+
condition: Case.argument(:filter, condition, block))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @private
|
|
50
|
+
def to_arel(table, model)
|
|
51
|
+
return aggregate(operand, table, model) unless condition
|
|
52
|
+
|
|
53
|
+
# A family without a FILTER clause gets the CASE that means the same.
|
|
54
|
+
# An aggregate passes over a NULL, so the case that yields nothing for
|
|
55
|
+
# the rows the condition misses is the same aggregate over the same
|
|
56
|
+
# rows -- count(*) has no operand to keep, and counts a 1 instead.
|
|
57
|
+
unless Dialect.for(model).filter_supported?
|
|
58
|
+
kept = Case.new.when(condition).then(operand == :* ? 1 : operand)
|
|
59
|
+
return aggregate(kept, table, model)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
aggregate(operand, table, model).filter(condition.to_arel(table, model))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def aggregate(over, table, model)
|
|
67
|
+
arel_operand = to_arel_operand(over, table, model)
|
|
68
|
+
return arel_operand.count(distinct) if function == :count
|
|
69
|
+
call = arel_operand.public_send(function)
|
|
70
|
+
call.distinct = distinct
|
|
71
|
+
call
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The strings of a group joined into one, a separator between:
|
|
76
|
+
# `string_agg(:title, ", ")`, with `.order` for the order they are
|
|
77
|
+
# joined in. Every family has it under a name of its own with the
|
|
78
|
+
# ORDER BY in a place of its own, and Arel has no node for an ORDER BY
|
|
79
|
+
# inside a call, so the dialect writes the call. A NULL is passed
|
|
80
|
+
# over as by any aggregate, so the CASE that stands in for FILTER
|
|
81
|
+
# means the same here and is not refused as the JSON aggregates' is.
|
|
82
|
+
class StringAggregate < Node
|
|
83
|
+
include Predications
|
|
84
|
+
include Windowing
|
|
85
|
+
|
|
86
|
+
# @private
|
|
87
|
+
attr_reader :operand, :separator, :orders, :condition
|
|
88
|
+
|
|
89
|
+
def initialize(operand, separator, orders: [], condition: nil)
|
|
90
|
+
unless separator.is_a?(::String)
|
|
91
|
+
raise ArgumentError, "#{separator.inspect} is not a String separator"
|
|
92
|
+
end
|
|
93
|
+
@operand = operand
|
|
94
|
+
@separator = separator
|
|
95
|
+
@orders = orders
|
|
96
|
+
@condition = condition
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The order the strings are joined in: columns, or orderings such as
|
|
100
|
+
# `:title.desc`.
|
|
101
|
+
# @return [AST::StringAggregate]
|
|
102
|
+
def order(*exprs)
|
|
103
|
+
raise ArgumentError, "order needs an expression" if exprs.empty?
|
|
104
|
+
StringAggregate.new(operand, separator, orders: orders + exprs, condition: condition)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `FILTER (WHERE condition)`, as {Aggregate#filter}.
|
|
108
|
+
# @return [AST::StringAggregate]
|
|
109
|
+
def filter(condition = nil, &block)
|
|
110
|
+
StringAggregate.new(operand, separator, orders: orders,
|
|
111
|
+
condition: Case.argument(:filter, condition, block))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @private
|
|
115
|
+
def check_window(model)
|
|
116
|
+
Dialect.for(model).check_string_aggregate_window(model)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @private
|
|
120
|
+
def to_arel(table, model)
|
|
121
|
+
dialect = Dialect.for(model)
|
|
122
|
+
kept = condition && !dialect.filter_supported? ?
|
|
123
|
+
Case.new.when(condition).then(operand) : operand
|
|
124
|
+
call = dialect.string_agg(
|
|
125
|
+
to_arel_argument(kept, table, model), separator,
|
|
126
|
+
orders.map { |expr| to_arel_operand(expr, table, model) },
|
|
127
|
+
string_operand?(model), model)
|
|
128
|
+
return call unless condition && dialect.filter_supported?
|
|
129
|
+
Arel::Nodes::Filter.new(call, condition.to_arel(table, model))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
# Whether the operand is a column the model declares a string.
|
|
134
|
+
# PostgreSQL asks, its STRING_AGG taking text and nothing else; the
|
|
135
|
+
# others convert for themselves.
|
|
136
|
+
def string_operand?(model)
|
|
137
|
+
operand.is_a?(::Symbol) &&
|
|
138
|
+
%i[string text].include?(model.type_for_attribute(operand).type)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A function call, `UPPER(name)`: what the scalar functions of
|
|
143
|
+
# {BlockContext} build, and {BlockContext#fn} for a function the list
|
|
144
|
+
# does not name.
|
|
145
|
+
class Function < Node
|
|
146
|
+
include Predications
|
|
147
|
+
include Arithmetics
|
|
148
|
+
include Windowing
|
|
149
|
+
|
|
150
|
+
# @private
|
|
151
|
+
attr_reader :name, :args
|
|
152
|
+
|
|
153
|
+
def initialize(name, args)
|
|
154
|
+
@name = name
|
|
155
|
+
@args = args
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @private
|
|
159
|
+
def to_arel(table, model)
|
|
160
|
+
arel_args = args.map { |arg| to_arel_argument(arg, table, model) }
|
|
161
|
+
Arel::Nodes::NamedFunction.new(name, arel_args)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ROW_NUMBER and its kind: functions that say nothing without a window.
|
|
166
|
+
# On its own this refuses rather than reaching the database as an error
|
|
167
|
+
# there; over asks it for call_arel instead.
|
|
168
|
+
class WindowFunction < Function
|
|
169
|
+
# @private
|
|
170
|
+
alias_method :call_arel, :to_arel
|
|
171
|
+
|
|
172
|
+
# @private
|
|
173
|
+
def to_arel(_table, _model)
|
|
174
|
+
raise ArgumentError, "#{name.downcase} is a window function; it needs over"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# EXTRACT(field FROM expr). The field is grammar rather than a value --
|
|
179
|
+
# a keyword the adapter reads bare -- so it has to be a plain name,
|
|
180
|
+
# which Arel upcases on the way out.
|
|
181
|
+
class Extract < Node
|
|
182
|
+
include Predications
|
|
183
|
+
include Arithmetics
|
|
184
|
+
|
|
185
|
+
# @private
|
|
186
|
+
attr_reader :field, :operand
|
|
187
|
+
|
|
188
|
+
def initialize(field, operand)
|
|
189
|
+
@field = AST.check_name(field, ALIAS_NAME, "extract field")
|
|
190
|
+
@operand = operand
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @private
|
|
194
|
+
def to_arel(table, model)
|
|
195
|
+
Arel::Nodes::Extract.new(to_arel_argument(operand, table, model), field.to_s)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# CAST(expr AS type). The type is grammar too, written into the SQL as
|
|
200
|
+
# given -- it is the adapter's own name for the type, and whether it
|
|
201
|
+
# exists is the database's to say -- so it has to look like one:
|
|
202
|
+
# a plain name, at most parenthesized with lengths.
|
|
203
|
+
class Cast < Node
|
|
204
|
+
include Predications
|
|
205
|
+
include Arithmetics
|
|
206
|
+
|
|
207
|
+
# @private
|
|
208
|
+
attr_reader :operand, :sql_type
|
|
209
|
+
|
|
210
|
+
def initialize(operand, sql_type)
|
|
211
|
+
@operand = operand
|
|
212
|
+
@sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @private
|
|
216
|
+
def to_arel(table, model)
|
|
217
|
+
Arel::Nodes::NamedFunction.new(
|
|
218
|
+
"CAST",
|
|
219
|
+
[Arel::Nodes::As.new(to_arel_argument(operand, table, model),
|
|
220
|
+
Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# CURRENT_TIMESTAMP and its relatives, what the SQL grammar calls a
|
|
225
|
+
# datetime value function. The grammar has them bare, and PostgreSQL
|
|
226
|
+
# and SQLite reject them written as calls, so unlike Function the name
|
|
227
|
+
# is emitted without parentheses. A precision is the one thing that
|
|
228
|
+
# does go into parentheses, and it is written into the SQL as given, so
|
|
229
|
+
# only an Integer is accepted.
|
|
230
|
+
class DatetimeValueFunction < Node
|
|
231
|
+
include Predications
|
|
232
|
+
include Arithmetics
|
|
233
|
+
|
|
234
|
+
# @private
|
|
235
|
+
attr_reader :name, :precision
|
|
236
|
+
|
|
237
|
+
def initialize(name, precision = nil)
|
|
238
|
+
unless precision.nil? || precision.is_a?(Integer)
|
|
239
|
+
raise ArgumentError,
|
|
240
|
+
"#{precision.inspect} is not an Integer precision"
|
|
241
|
+
end
|
|
242
|
+
@name = name
|
|
243
|
+
@precision = precision
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @private
|
|
247
|
+
def to_arel(_table, _model)
|
|
248
|
+
Arel::Nodes::SqlLiteral.new(
|
|
249
|
+
precision ? "#{name}(#{precision})" : name)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/refined/ast/node"
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module Refined
|
|
7
|
+
module AST
|
|
8
|
+
# GROUP BY GROUPING SETS / ROLLUP / CUBE: several groupings asked for at
|
|
9
|
+
# once, the totals of each coming back beside the rows. PostgreSQL has
|
|
10
|
+
# all three and the MySQL family rollup alone; the block raises for the
|
|
11
|
+
# rest before it gets this far.
|
|
12
|
+
#
|
|
13
|
+
# Each set is a list of its own, so grouping_sets takes lists and rollup
|
|
14
|
+
# and cube take the columns themselves.
|
|
15
|
+
class GroupingSets < Node
|
|
16
|
+
# @private
|
|
17
|
+
KINDS = {
|
|
18
|
+
grouping_sets: Arel::Nodes::GroupingSet,
|
|
19
|
+
rollup: Arel::Nodes::RollUp,
|
|
20
|
+
cube: Arel::Nodes::Cube,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
attr_reader :kind, :sets
|
|
25
|
+
|
|
26
|
+
def initialize(kind, sets)
|
|
27
|
+
raise ArgumentError, "#{kind} needs something to group by" if sets.empty?
|
|
28
|
+
@kind = kind
|
|
29
|
+
@sets = sets
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @private
|
|
33
|
+
def to_arel(table, model)
|
|
34
|
+
return with_rollup(table, model) if Dialect.for(model).grouping_by_with_rollup?
|
|
35
|
+
|
|
36
|
+
KINDS.fetch(kind).new(
|
|
37
|
+
if kind == :grouping_sets
|
|
38
|
+
sets.map do |set|
|
|
39
|
+
Arel::Nodes::GroupingElement.new(
|
|
40
|
+
Array(set).map { |column| to_arel_operand(column, table, model) })
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
sets.map { |column| to_arel_operand(column, table, model) }
|
|
44
|
+
end)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
# The MySQL family spells rollup WITH ROLLUP, trailing the whole
|
|
49
|
+
# group list rather than wrapping a list of its own -- which is also
|
|
50
|
+
# why a rollup cannot stand beside other group entries there. The
|
|
51
|
+
# columns are compiled by the connection's own visitor, so their
|
|
52
|
+
# quoting is the adapter's.
|
|
53
|
+
def with_rollup(table, model)
|
|
54
|
+
columns = sets.map { |column| to_arel_operand(column, table, model) }
|
|
55
|
+
sql = model.with_connection do |connection|
|
|
56
|
+
columns.map { |column| connection.visitor.compile(column) }.join(", ")
|
|
57
|
+
end
|
|
58
|
+
Arel.sql("#{sql} WITH ROLLUP")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|