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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +19 -22
  4. data/activerecord-refined.gemspec +4 -3
  5. data/docs/conditions.md +3 -1
  6. data/docs/ctes.md +2 -2
  7. data/docs/expressions.md +37 -20
  8. data/docs/functions.md +14 -10
  9. data/docs/index.md +2 -17
  10. data/docs/windows.md +2 -2
  11. data/examples/expressions.rb +21 -15
  12. data/lib/active_record/refined/ast/arithmetics.rb +137 -0
  13. data/lib/active_record/refined/ast/conditions.rb +451 -0
  14. data/lib/active_record/refined/ast/expressions.rb +304 -0
  15. data/lib/active_record/refined/ast/functions.rb +254 -0
  16. data/lib/active_record/refined/ast/grouping.rb +63 -0
  17. data/lib/active_record/refined/ast/json.rb +530 -0
  18. data/lib/active_record/refined/ast/node.rb +298 -0
  19. data/lib/active_record/refined/ast/ordering.rb +104 -0
  20. data/lib/active_record/refined/ast/predications.rb +384 -0
  21. data/lib/active_record/refined/ast/windows.rb +126 -0
  22. data/lib/active_record/refined/ast.rb +39 -2364
  23. data/lib/active_record/refined/block_context.rb +671 -0
  24. data/lib/active_record/refined/block_syntax.rb +128 -0
  25. data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
  26. data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
  27. data/lib/active_record/refined/dialect/oracle.rb +1 -7
  28. data/lib/active_record/refined/dialect/postgresql.rb +8 -0
  29. data/lib/active_record/refined/dialect/sql_server.rb +29 -5
  30. data/lib/active_record/refined/dialect/sqlite.rb +23 -0
  31. data/lib/active_record/refined/dialect.rb +132 -66
  32. data/lib/active_record/refined/query_methods.rb +444 -0
  33. data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
  34. data/lib/active_record/refined/writes.rb +72 -0
  35. data/lib/active_record/refined.rb +7 -1270
  36. data/lib/activerecord-refined.rb +0 -4
  37. 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