activerecord-refined 0.10.2 → 0.11.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.
@@ -6,10 +6,14 @@ module ActiveRecord
6
6
  # What MySQL and MariaDB share, which is most of it; the two part company
7
7
  # only in the Mysql and Mariadb subclasses.
8
8
  class MysqlCompat < Dialect
9
+ include MysqlishJsonFunctions
10
+
11
+ # @private
9
12
  FUNCTIONS = { trunc: "TRUNCATE", date_trunc: nil, format: nil }.freeze
10
13
 
11
14
  def full_outer_join_supported? = false
12
15
  def filter_supported? = false
16
+ def bitwise_operators_supported? = true
13
17
 
14
18
  def bit_count(expr, _model)
15
19
  AST::Function.new("BIT_COUNT", [expr])
@@ -41,6 +45,18 @@ module ActiveRecord
41
45
  "JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
42
46
  end
43
47
 
48
+ def json_keys(document, _model)
49
+ Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
50
+ end
51
+
52
+ # A Ruby document or boolean written where the JSON functions want
53
+ # JSON is marked as it by reading it out whole: JSON_EXTRACT($).
54
+ def json_argument(value, _model)
55
+ json = Arel::Nodes.build_quoted(JSON.generate(value))
56
+ Arel::Nodes::NamedFunction.new(
57
+ "JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
58
+ end
59
+
44
60
  def json_aggregate_filter_supported? = false
45
61
 
46
62
  # GROUP_CONCAT, its separator a keyword after the operand and after
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Refined
5
+ class Dialect
6
+ # The JSON editing and building functions MySQL and SQLite spell
7
+ # alike: JSON_SET and JSON_REMOVE over a $ path, and JSON_ARRAY and
8
+ # JSON_OBJECT with each key alternating with its value. The spellings
9
+ # are MySQL's, which SQLite's json1 took over, and standard in
10
+ # neither sense of the word -- SQL:2016 gives JSON_OBJECT a KEY k
11
+ # VALUE v syntax and has no editing functions at all -- so they live
12
+ # here as a family likeness for the two to include, not in the base
13
+ # as a default for everyone.
14
+ module MysqlishJsonFunctions
15
+ def json_set(document, _steps, dollar_path, value, expression, model)
16
+ Arel::Nodes::NamedFunction.new(
17
+ "JSON_SET",
18
+ [document, Arel::Nodes.build_quoted(dollar_path),
19
+ json_set_value(value, expression, model)])
20
+ end
21
+
22
+ def json_remove(document, dollar_paths, _steps, _model)
23
+ Arel::Nodes::NamedFunction.new(
24
+ "JSON_REMOVE",
25
+ [document, *dollar_paths.map { |path| Arel::Nodes.build_quoted(path) }])
26
+ end
27
+
28
+ def json_build(kind, keys, args, _model)
29
+ Arel::Nodes::NamedFunction.new(
30
+ kind == :array ? "JSON_ARRAY" : "JSON_OBJECT",
31
+ json_build_body(kind, keys, args))
32
+ end
33
+
34
+ private
35
+ # The value beside a path in JSON_SET: an expression as it is, a
36
+ # document or boolean as JSON, a bare scalar quoted.
37
+ def json_set_value(value, expression, model)
38
+ return expression if expression
39
+ return Arel::Nodes.build_quoted(value) unless json_document_value?(value)
40
+ json_argument(value, model)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -6,6 +6,7 @@ module ActiveRecord
6
6
  # Oracle, reached through oracle_enhanced. Loaded only when a query is
7
7
  # built for it.
8
8
  class Oracle < Dialect
9
+ # @private
9
10
  FUNCTIONS = {
10
11
  char_length: "LENGTH",
11
12
  degrees: nil, radians: nil, pi: nil, log2: nil, log10: nil,
@@ -44,13 +45,6 @@ module ActiveRecord
44
45
  Arel::Nodes::NamedFunction.new("JSON_EXISTS", [document, path])
45
46
  end
46
47
 
47
- # No JSON_KEYS, and the keys reach only through a JSON_TABLE unnest not
48
- # written yet.
49
- def json_keys(_document, model)
50
- raise NotImplementedError,
51
- "keys has no equivalent on #{model.connection_db_config.adapter}"
52
- end
53
-
54
48
  def json_argument(value, model)
55
49
  Arel.sql("#{quote(JSON.generate(value), model)} FORMAT JSON")
56
50
  end
@@ -5,6 +5,10 @@ module ActiveRecord
5
5
  class Dialect
6
6
  # PostgreSQL, and the adapters that answer for the same server.
7
7
  class Postgresql < Dialect
8
+ def array_comparisons_supported? = true
9
+ def bitwise_operators_supported? = true
10
+
11
+ # @private
8
12
  FUNCTIONS = { log2: nil, rand: "RANDOM" }.freeze
9
13
 
10
14
  # PostgreSQL counts the bits of a bit string rather than a number, so
@@ -19,6 +23,10 @@ module ActiveRecord
19
23
  Arel::Nodes::InfixOperation.new("#", left, right)
20
24
  end
21
25
 
26
+ def excluded(column, _model)
27
+ AST::Column.new(:excluded, column)
28
+ end
29
+
22
30
  def json_path(document, _dollar_path, steps, json_value, _model)
23
31
  Arel::Nodes::InfixOperation.new(
24
32
  json_value ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps))
@@ -6,12 +6,21 @@ module ActiveRecord
6
6
  # Microsoft SQL Server, reached through the sqlserver adapter over
7
7
  # tiny_tds. Loaded only when a query is built for it.
8
8
  class SqlServer < Dialect
9
- # LEN is its length; it has no printf FORMAT, no per-row random it
10
- # would spell RAND, no date_trunc, and none of the bit aggregates.
11
- # Of the datetime value functions it has CURRENT_TIMESTAMP alone:
12
- # the other four are reserved words there that stand for nothing.
9
+ # LEN is its length, CEILING its ceil, ATN2 its atan2, and LOG --
10
+ # natural by default -- its ln; SUBSTRING insists on the length, so
11
+ # substr wants all three arguments here. It has no printf FORMAT, no
12
+ # per-row random it would spell RAND, no date_trunc, no NOW or LOG2,
13
+ # none of the bit aggregates, no MOD beside the % operator, and no
14
+ # numeric TRUNC. log is nil for a sharper reason: its LOG(x, base)
15
+ # takes the arguments in the other order, so a rename would quietly
16
+ # swap them. Of the datetime value functions it has
17
+ # CURRENT_TIMESTAMP alone: the other four are reserved words there
18
+ # that stand for nothing.
19
+ # @private
13
20
  FUNCTIONS = {
14
- char_length: "LEN",
21
+ char_length: "LEN", length: "LEN", substr: "SUBSTRING",
22
+ ceil: "CEILING", atan2: "ATN2", ln: "LOG",
23
+ mod: nil, trunc: nil, log: nil, log2: nil, now: nil,
15
24
  format: nil, rand: nil, date_trunc: nil,
16
25
  bit_and: nil, bit_or: nil, bit_xor: nil,
17
26
  current_date: nil, current_time: nil, localtime: nil, localtimestamp: nil,
@@ -20,6 +29,15 @@ module ActiveRecord
20
29
  # No FILTER clause, so the aggregate node builds the CASE instead.
21
30
  def filter_supported? = false
22
31
 
32
+ # The operators are all here, the shifts included -- CI executes them.
33
+ def bitwise_operators_supported? = true
34
+
35
+ # ^ is XOR here as on MySQL, sparing the (a | b) - (a & b) the base
36
+ # spells for SQLite's sake.
37
+ def bitwise_xor(left, right)
38
+ Arel::Nodes::BitwiseXor.new(left, right)
39
+ end
40
+
23
41
  # No EXTRACT (it has DATEPART), and its datetime value functions take
24
42
  # no precision.
25
43
  def extract_supported? = false
@@ -60,6 +78,12 @@ module ActiveRecord
60
78
  "string_agg over a window has no equivalent on #{model.connection_db_config.adapter}"
61
79
  end
62
80
 
81
+ # No JSON aggregates, so the SQL:2016 names the base keeps would
82
+ # reach the server as functions it does not have.
83
+ def json_aggregate_name(kind)
84
+ raise NotImplementedError, "json_#{kind} has no equivalent on SQL Server"
85
+ end
86
+
63
87
  # dig_text reads a scalar out with JSON_VALUE; dig keeps JSON with
64
88
  # JSON_QUERY, which returns a fragment and NULL for a scalar leaf.
65
89
  def json_path(document, dollar_path, _steps, json_value, _model)
@@ -6,6 +6,9 @@ module ActiveRecord
6
6
  # SQLite: the fewest of the extras, and a JSON path through its own
7
7
  # operators.
8
8
  class Sqlite < Dialect
9
+ include MysqlishJsonFunctions
10
+
11
+ # @private
9
12
  FUNCTIONS = {
10
13
  char_length: "LENGTH", greatest: "MAX", least: "MIN",
11
14
  now: nil, date_trunc: nil, rand: "RANDOM",
@@ -16,6 +19,7 @@ module ActiveRecord
16
19
  def datetime_precision_supported? = false
17
20
  def extract_supported? = false
18
21
  def quantifiers_supported? = false
22
+ def bitwise_operators_supported? = true
19
23
 
20
24
  def check_lateral(model)
21
25
  raise NotImplementedError,
@@ -32,6 +36,21 @@ module ActiveRecord
32
36
  date_only ? "date" : "datetime", [date, Arel::Nodes.build_quoted(modifier)])
33
37
  end
34
38
 
39
+ # -> keeps the value's type where ->> gives text, so dig_text casts
40
+ # to text only to make the comparison portable.
41
+ def json_path(document, dollar_path, _steps, json_value, _model)
42
+ extracted = Arel::Nodes::InfixOperation.new(
43
+ json_value ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
44
+ return extracted if json_value
45
+ Arel::Nodes::NamedFunction.new(
46
+ "CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
47
+ end
48
+
49
+ # json_type at the path answers NULL where nothing stands there.
50
+ def json_has_key(document, _name, path, _model)
51
+ Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
52
+ end
53
+
35
54
  def json_keys(document, model)
36
55
  sql = compile(document, model)
37
56
  Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
@@ -47,6 +66,10 @@ module ActiveRecord
47
66
  kind == :arrayagg ? "json_group_array" : "json_group_object"
48
67
  end
49
68
 
69
+ def excluded(column, _model)
70
+ AST::Column.new(:excluded, column)
71
+ end
72
+
50
73
  # group_concat, with the ORDER BY inside the call from 3.44 on.
51
74
  def string_agg(operand, separator, orders, _string, model)
52
75
  string_agg_call("group_concat", operand, separator, orders, model)
@@ -8,11 +8,15 @@ module ActiveRecord
8
8
  # and asked, rather than branched on, for whatever a query builds
9
9
  # differently from one database to the next. The base is the standard
10
10
  # spelling an unclassified adapter keeps; each subclass overrides only
11
- # where its family departs from it.
11
+ # where its family departs from it. Where no spelling is anyone's
12
+ # standard -- most of JSON -- the base refuses instead, and every family
13
+ # carries its own.
12
14
  #
13
15
  # The families are loaded as they are met: an application on PostgreSQL
14
16
  # never loads the Oracle class, whose adapter it will never resolve to.
15
17
  class Dialect
18
+ autoload :MysqlishJsonFunctions,
19
+ "active_record/refined/dialect/mysqlish_json_functions"
16
20
  autoload :Sqlite, "active_record/refined/dialect/sqlite"
17
21
  autoload :Postgresql, "active_record/refined/dialect/postgresql"
18
22
  autoload :MysqlCompat, "active_record/refined/dialect/mysql_compat"
@@ -84,10 +88,23 @@ module ActiveRecord
84
88
 
85
89
  # The scalar and datetime functions a family spells differently, or has
86
90
  # none of. A name it does not list it spells like the method, upper
87
- # cased; a nil says it has no equivalent, and the block raises. The base
88
- # -- an unclassified adapter -- keeps every standard name.
89
- FUNCTIONS = {}.freeze
90
-
91
+ # cased; a nil says it has no equivalent, and the block raises. The
92
+ # base -- an unclassified adapter -- keeps the names most families
93
+ # share and nils the ones that are one or two families' own: printf
94
+ # FORMAT, whose name MySQL hands to a different function entirely,
95
+ # RAND, DATE_TRUNC, NOW, LOG2 and the three bit aggregates. A family
96
+ # that has one of them says so in a table of its own, since the tables
97
+ # shadow rather than merge.
98
+ FUNCTIONS = {
99
+ format: nil, rand: nil, date_trunc: nil, now: nil, log2: nil,
100
+ bit_and: nil, bit_or: nil, bit_xor: nil,
101
+ }.freeze
102
+
103
+ # The name this family spells a function with, asked for as the block
104
+ # builds the call; {FUNCTIONS} is where the answer is looked up.
105
+ # @param name [Symbol] the method's own name, as {BlockContext} has it
106
+ # @return [String]
107
+ # @raise [NotImplementedError] where the family has no equivalent
91
108
  def function_name(name, model)
92
109
  functions = self.class::FUNCTIONS
93
110
  return name.to_s.upcase unless functions.key?(name)
@@ -96,9 +113,19 @@ module ActiveRecord
96
113
  "#{name} has no equivalent on #{model.connection_db_config.adapter}")
97
114
  end
98
115
 
116
+ # Whether the datetime value functions take a precision,
117
+ # `current_timestamp(3)`. SQLite and SQL Server take none.
99
118
  def datetime_precision_supported? = true
119
+
120
+ # Whether the family has EXTRACT. SQLite spells the fields as strftime
121
+ # formats and SQL Server as DATEPART, so neither answers to the name.
100
122
  def extract_supported? = true
123
+
124
+ # Whether a comparison can be quantified with ANY or ALL. SQLite has
125
+ # neither.
101
126
  def quantifiers_supported? = true
127
+
128
+ # Whether a FULL OUTER JOIN can be written. The MySQL family has none.
102
129
  def full_outer_join_supported? = true
103
130
 
104
131
  # The FILTER clause, which restricts an aggregate to the rows a condition
@@ -106,6 +133,16 @@ module ActiveRecord
106
133
  # built by the aggregate node.
107
134
  def filter_supported? = true
108
135
 
136
+ # The bitwise operators, & | ^ << >> and ~. No SQL standard has them,
137
+ # so unlike the capabilities above the base refuses and each family
138
+ # that has the operators says so itself -- which of the seven is every
139
+ # one but Oracle, whose single bit operation is the BITAND function.
140
+ def bitwise_operators_supported? = false
141
+
142
+ # The array comparisons -- @>, <@ and && against an array column.
143
+ # The type and its operators are PostgreSQL's alone.
144
+ def array_comparisons_supported? = false
145
+
109
146
  # A lateral join is allowed to stand unless the family refuses it here.
110
147
  def check_lateral(_model); end
111
148
 
@@ -118,10 +155,13 @@ module ActiveRecord
118
155
  "bit_count has no equivalent on #{model.connection_db_config.adapter}"
119
156
  end
120
157
 
121
- # The row an upsert could not insert. PostgreSQL and SQLite name it;
122
- # MySQL spells the same thing VALUES(column) and overrides.
123
- def excluded(column, _model)
124
- AST::Column.new(:excluded, column)
158
+ # The row an upsert could not insert. `excluded` is PostgreSQL's name
159
+ # for it, which SQLite took over and the MySQL family spells
160
+ # VALUES(column), so each of the three carries its own; the families
161
+ # without an upsert have nothing for the name to stand in.
162
+ def excluded(_column, model)
163
+ raise NotImplementedError,
164
+ "excluded has no equivalent on #{model.connection_db_config.adapter}"
125
165
  end
126
166
 
127
167
  # true? / false? and their negations. The standard spells them with the
@@ -157,26 +197,32 @@ module ActiveRecord
157
197
  Arel::Nodes::InfixOperation.new(subtract ? :- : :+, date, interval))
158
198
  end
159
199
 
160
- # XOR, which no two families spell alike. The standard is the two
161
- # operations it is made of, naming each operand twice, as SQLite needs;
162
- # PostgreSQL and the MySQL family have an operator and override.
200
+ # XOR, which no two families spell alike. The default is the two
201
+ # operations it is made of, naming each operand twice -- built only
202
+ # from the & and | a family has just claimed through
203
+ # {#bitwise_operators_supported?}, so wherever it can be reached at
204
+ # all it works. Of the five that claim them, SQLite alone keeps it;
205
+ # the rest have an operator of their own and override.
163
206
  def bitwise_xor(left, right)
164
207
  Arel::Nodes::Subtraction.new(
165
208
  Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
166
209
  Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
167
210
  end
168
211
 
169
- # --- Reading JSON. The defaults are what an unclassified adapter gets,
170
- # which is SQLite's operators for a path and its functions elsewhere.
171
-
172
- # dig / dig_text. The standard is SQLite's -> and ->>, whose ->> keeps
173
- # the value's type, so dig_text casts to text for a portable comparison.
174
- def json_path(document, dollar_path, _steps, json_value, _model)
175
- extracted = Arel::Nodes::InfixOperation.new(
176
- json_value ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
177
- return extracted if json_value
178
- Arel::Nodes::NamedFunction.new(
179
- "CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
212
+ # --- Reading JSON. No two families spell it alike and the standard
213
+ # names only part of it, so the base refuses each piece and every
214
+ # family carries its own; what an unclassified adapter would get
215
+ # from any one family's spelling is a query that means nothing on
216
+ # the next.
217
+
218
+ # dig / dig_text. Nothing reaches every family: SQLite and PostgreSQL
219
+ # have operators of their own, MySQL its functions, and the two that
220
+ # spell it as SQL:2016's JSON_VALUE and JSON_QUERY -- Oracle and SQL
221
+ # Server -- differ over what a scalar leaf comes back as. Every
222
+ # family overrides.
223
+ def json_path(_document, _dollar_path, _steps, _json_value, model)
224
+ raise NotImplementedError,
225
+ "dig has no equivalent on #{model.connection_db_config.adapter}"
180
226
  end
181
227
 
182
228
  # A Ruby value on the JSON side of a comparison belongs to a JSON type;
@@ -187,69 +233,94 @@ module ActiveRecord
187
233
  "#{model.connection_db_config.adapter}; dig_text gives the value"
188
234
  end
189
235
 
236
+ # contains?: whether the document holds what is given. The standard has
237
+ # no equivalent; PostgreSQL has @> and the MySQL family JSON_CONTAINS,
238
+ # and both override.
190
239
  def json_contains(_document, _json, model)
191
240
  raise NotImplementedError,
192
241
  "contains? has no equivalent on #{model.connection_db_config.adapter}"
193
242
  end
194
243
 
195
- def json_has_key(document, _name, path, _model)
196
- Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
244
+ # key?: whether the object has the key. SQL:2016 spells it
245
+ # JSON_EXISTS, which of the five only Oracle answers to; PostgreSQL has
246
+ # the ? operator, the MySQL family JSON_CONTAINS_PATH, SQLite json_type
247
+ # at the path and SQL Server JSON_PATH_EXISTS, so every family
248
+ # overrides.
249
+ #
250
+ # The key arrives spelled both ways -- bare in `name`, as a $ path in
251
+ # `path` -- since a family reads it as one or as the other.
252
+ def json_has_key(_document, _name, _path, model)
253
+ raise NotImplementedError,
254
+ "key? has no equivalent on #{model.connection_db_config.adapter}"
197
255
  end
198
256
 
199
- def json_keys(document, _model)
200
- Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
257
+ # keys: the keys of an object as a JSON array. JSON_KEYS is the MySQL
258
+ # family's own; SQLite and PostgreSQL gather theirs through a subquery
259
+ # over their key-listing functions, and Oracle and SQL Server would
260
+ # reach them only through table unnests not written here.
261
+ def json_keys(_document, model)
262
+ raise NotImplementedError,
263
+ "keys has no equivalent on #{model.connection_db_config.adapter}"
201
264
  end
202
265
 
203
266
  # --- Writing JSON. A Ruby document or boolean has to be told apart from
204
267
  # a bare scalar, and embedded as the JSON it spells.
205
268
 
269
+ # The test the writing hooks share: a Hash, an Array or a boolean is a
270
+ # document, and anything else a bare scalar.
206
271
  def json_document_value?(value)
207
272
  value.is_a?(::Hash) || value.is_a?(::Array) || value == true || value == false
208
273
  end
209
274
 
210
275
  # A Ruby document or boolean written where one of the JSON functions
211
- # wants JSON. The standard marks the literal with JSON_EXTRACT($);
212
- # SQLite has json() and Oracle FORMAT JSON, and both override.
213
- def json_argument(value, _model)
214
- json = Arel::Nodes.build_quoted(JSON.generate(value))
215
- Arel::Nodes::NamedFunction.new("JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
276
+ # wants JSON. Every family marks the literal its own way --
277
+ # JSON_EXTRACT($) on MySQL, json() on SQLite, FORMAT JSON on Oracle,
278
+ # JSON_QUERY on SQL Server -- and none of the marks is another's.
279
+ def json_argument(_value, model)
280
+ raise NotImplementedError,
281
+ "a document written as JSON has no equivalent on " \
282
+ "#{model.connection_db_config.adapter}"
216
283
  end
217
284
 
218
- # bury: setting a value at a path. The standard is JSON_SET; PostgreSQL
219
- # has jsonb_set and Oracle JSON_TRANSFORM, and both override.
220
- def json_set(document, _steps, dollar_path, value, expression, model)
221
- Arel::Nodes::NamedFunction.new(
222
- "JSON_SET",
223
- [document, Arel::Nodes.build_quoted(dollar_path),
224
- json_set_value(value, expression, model)])
285
+ # bury: setting a value at a path. The standard has no editing
286
+ # functions at all: JSON_SET is {MysqlishJsonFunctions}', jsonb_set
287
+ # PostgreSQL's, JSON_TRANSFORM Oracle's and JSON_MODIFY SQL Server's.
288
+ def json_set(_document, _steps, _dollar_path, _value, _expression, model)
289
+ raise NotImplementedError,
290
+ "bury has no equivalent on #{model.connection_db_config.adapter}"
225
291
  end
226
292
 
227
- # except: removing keys. The standard removes a path apiece with
228
- # JSON_REMOVE; PostgreSQL subtracts an array of keys and Oracle removes
229
- # through JSON_TRANSFORM, and both override.
230
- def json_remove(document, dollar_paths, _steps, _model)
231
- Arel::Nodes::NamedFunction.new(
232
- "JSON_REMOVE",
233
- [document, *dollar_paths.map { |path| Arel::Nodes.build_quoted(path) }])
293
+ # except: removing keys, for which the standard likewise has nothing.
294
+ # {MysqlishJsonFunctions} removes a path apiece with JSON_REMOVE,
295
+ # PostgreSQL subtracts an array of keys, Oracle and SQL Server edit
296
+ # through JSON_TRANSFORM and JSON_MODIFY.
297
+ def json_remove(_document, _dollar_paths, _steps, model)
298
+ raise NotImplementedError,
299
+ "except has no equivalent on #{model.connection_db_config.adapter}"
234
300
  end
235
301
 
236
- # json_array / json_object built in the row. The standard says JSON_ARRAY
237
- # and JSON_OBJECT with the values (and keys alternating); PostgreSQL has
238
- # the jsonb_build_* pair and Oracle a keyword syntax, and both override.
239
- def json_build(kind, keys, args, _model)
240
- Arel::Nodes::NamedFunction.new(
241
- kind == :array ? "JSON_ARRAY" : "JSON_OBJECT", json_build_body(kind, keys, args))
302
+ # json_array / json_object built in the row. JSON_ARRAY(a, b) is
303
+ # SQL:2016, but JSON_OBJECT is where the syntaxes part -- the standard
304
+ # pairs each key as KEY k VALUE v, {MysqlishJsonFunctions} alternates
305
+ # them, SQL Server writes k : v -- so the pair travels together and
306
+ # each family says its own; SQL Server's is not written here yet.
307
+ def json_build(kind, _keys, _args, model)
308
+ raise NotImplementedError,
309
+ "json_#{kind} has no equivalent on #{model.connection_db_config.adapter}"
242
310
  end
243
311
 
244
- # A document or boolean built into json_array/json_object. The standard
245
- # marks it JSON as bury does; PostgreSQL casts to jsonb and overrides.
312
+ # A document or boolean built into json_array/json_object. The default
313
+ # rides through {#json_argument}, and refuses or serves with it;
314
+ # PostgreSQL casts to jsonb and overrides.
246
315
  def json_build_argument(value, model)
247
316
  json_argument(value, model)
248
317
  end
249
318
 
250
319
  # json_arrayagg / json_objectagg gather rows into a document. The
251
- # standard names are JSON_ARRAYAGG and JSON_OBJECTAGG; SQLite and
252
- # PostgreSQL have their own and override.
320
+ # names are SQL:2016's own, which the MySQL family and Oracle answer
321
+ # to, so unlike the rest of JSON the base keeps them; SQLite and
322
+ # PostgreSQL have names of their own, and SQL Server, which has no
323
+ # JSON aggregates, refuses.
253
324
  def json_aggregate_name(kind)
254
325
  kind == :arrayagg ? "JSON_ARRAYAGG" : "JSON_OBJECTAGG"
255
326
  end
@@ -286,6 +357,8 @@ module ActiveRecord
286
357
  # --- Grouping. GROUPING SETS, ROLLUP and CUBE, which the standard has
287
358
  # none of; PostgreSQL has all three and the MySQL family rollup alone.
288
359
 
360
+ # Whether the family has the kind of grouping asked for, one of
361
+ # `:grouping_sets`, `:rollup` and `:cube`.
289
362
  def grouping_supported?(_kind) = false
290
363
 
291
364
  # The MySQL family spells rollup WITH ROLLUP, trailing the group list
@@ -293,16 +366,9 @@ module ActiveRecord
293
366
  def grouping_by_with_rollup? = false
294
367
 
295
368
  protected
296
- # The value beside a path in JSON_SET: an expression as it is, a
297
- # document or boolean as JSON, a bare scalar quoted.
298
- def json_set_value(value, expression, model)
299
- return expression if expression
300
- return Arel::Nodes.build_quoted(value) unless json_document_value?(value)
301
- json_argument(value, model)
302
- end
303
-
304
- # The arguments to JSON_ARRAY/JSON_OBJECT: an array's values as they
305
- # are, an object's keys alternating with them.
369
+ # The values of JSON_ARRAY as they are, JSON_OBJECT's keys
370
+ # alternating with theirs -- the shape MysqlishJsonFunctions and
371
+ # PostgreSQL's jsonb_build_* pair both take.
306
372
  def json_build_body(kind, keys, args)
307
373
  return args if kind == :array
308
374
  keys.zip(args).flat_map { |key, arg| [Arel::Nodes.build_quoted(key), arg] }