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.
- checksums.yaml +4 -4
- data/docs/ctes.md +2 -2
- data/docs/expressions.md +27 -13
- data/docs/functions.md +14 -10
- data/docs/index.md +1 -16
- data/docs/windows.md +2 -2
- data/examples/expressions.rb +13 -7
- data/lib/active_record/refined/ast.rb +330 -55
- 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.rb +59 -22
- data/lib/activerecord-refined/version.rb +6 -1
- metadata +2 -1
|
@@ -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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
|
88
|
-
# -- an unclassified adapter -- keeps
|
|
89
|
-
|
|
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.
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
161
|
-
# operations it is made of, naming each operand twice
|
|
162
|
-
#
|
|
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.
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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.
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
245
|
-
#
|
|
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
|
-
#
|
|
252
|
-
#
|
|
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
|
|
297
|
-
#
|
|
298
|
-
|
|
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] }
|