activerecord-refined 0.9.0 → 0.10.1
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 +17 -0
- data/README.md +61 -1064
- data/activerecord-refined.gemspec +8 -7
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/index.md +70 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +18 -0
- data/examples/expressions.rb +35 -5
- data/lib/active_record/refined/ast.rb +461 -246
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +682 -192
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +1 -0
- metadata +43 -16
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -104
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -393
- data/Gemfile +0 -14
- data/Rakefile +0 -53
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2999
- data/test/test_helper.rb +0 -238
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# MariaDB, which answers to the MySQL adapter but has no JSON type and no
|
|
7
|
+
# LATERAL.
|
|
8
|
+
class Mariadb < MysqlCompat
|
|
9
|
+
def check_lateral(_model)
|
|
10
|
+
raise NotImplementedError, "a lateral join has no equivalent on MariaDB"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# MariaDB has no JSON type, so a JSON comparison is refused.
|
|
14
|
+
def json_literal(_json, _model)
|
|
15
|
+
raise NotImplementedError,
|
|
16
|
+
"a JSON comparison has no equivalent on MariaDB; dig_text gives the value"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def check_json_aggregate_window(source, _model)
|
|
20
|
+
raise NotImplementedError, "#{source} over a window has no equivalent on MariaDB"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# MySQL proper, which has a JSON type of its own where MariaDB has only
|
|
7
|
+
# the text.
|
|
8
|
+
class Mysql < MysqlCompat
|
|
9
|
+
# MySQL has a JSON type, so a Ruby value is cast to it; a bare string
|
|
10
|
+
# beside JSON would be a JSON string, outranking every number.
|
|
11
|
+
def json_literal(json, _model)
|
|
12
|
+
Arel::Nodes::NamedFunction.new(
|
|
13
|
+
"CAST", [Arel::Nodes::As.new(json, Arel::Nodes::SqlLiteral.new("JSON"))])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# What MySQL and MariaDB share, which is most of it; the two part company
|
|
7
|
+
# only in the Mysql and Mariadb subclasses.
|
|
8
|
+
class MysqlCompat < Dialect
|
|
9
|
+
FUNCTIONS = { trunc: "TRUNCATE", date_trunc: nil, format: nil }.freeze
|
|
10
|
+
|
|
11
|
+
def full_outer_join_supported? = false
|
|
12
|
+
def filter_supported? = false
|
|
13
|
+
|
|
14
|
+
def bit_count(expr, _model)
|
|
15
|
+
AST::Function.new("BIT_COUNT", [expr])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The excluded row is VALUES(column), which takes the column bare.
|
|
19
|
+
def excluded(column, model)
|
|
20
|
+
quoted = model.with_connection { |connection| connection.quote_column_name(column) }
|
|
21
|
+
AST::Function.new("VALUES", [Arel::Nodes::SqlLiteral.new(quoted)])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bitwise_xor(left, right)
|
|
25
|
+
Arel::Nodes::BitwiseXor.new(left, right)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def json_path(document, dollar_path, _steps, json_value, _model)
|
|
29
|
+
extracted = Arel::Nodes::NamedFunction.new(
|
|
30
|
+
"JSON_EXTRACT", [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
31
|
+
return extracted if json_value
|
|
32
|
+
Arel::Nodes::NamedFunction.new("JSON_UNQUOTE", [extracted])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def json_contains(document, json, _model)
|
|
36
|
+
Arel::Nodes::NamedFunction.new("JSON_CONTAINS", [document, json])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def json_has_key(document, _name, path, _model)
|
|
40
|
+
Arel::Nodes::NamedFunction.new(
|
|
41
|
+
"JSON_CONTAINS_PATH", [document, Arel::Nodes.build_quoted("one"), path])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def json_aggregate_filter_supported? = false
|
|
45
|
+
|
|
46
|
+
# GROUP_CONCAT, its separator a keyword after the operand and after
|
|
47
|
+
# the ORDER BY when there is one.
|
|
48
|
+
def string_agg(operand, separator, orders, _string, model)
|
|
49
|
+
order = orders.empty? ? "" : " ORDER BY #{compile_list(orders, model)}"
|
|
50
|
+
Arel.sql("GROUP_CONCAT(#{compile(operand, model)}#{order} " \
|
|
51
|
+
"SEPARATOR #{quote(separator, model)})")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_string_aggregate_window(model)
|
|
55
|
+
raise NotImplementedError,
|
|
56
|
+
"string_agg over a window has no equivalent on #{model.connection_db_config.adapter}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def json_list_by_element? = true
|
|
60
|
+
|
|
61
|
+
def grouping_supported?(kind) = kind == :rollup
|
|
62
|
+
|
|
63
|
+
def grouping_by_with_rollup? = true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# Oracle, reached through oracle_enhanced. Loaded only when a query is
|
|
7
|
+
# built for it.
|
|
8
|
+
class Oracle < Dialect
|
|
9
|
+
FUNCTIONS = {
|
|
10
|
+
char_length: "LENGTH",
|
|
11
|
+
degrees: nil, radians: nil, pi: nil, log2: nil, log10: nil,
|
|
12
|
+
now: nil, date_trunc: nil, rand: nil, format: nil,
|
|
13
|
+
bit_and: nil, bit_or: nil, bit_xor: nil,
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# An INTERVAL literal would do, but its leading field is two digits
|
|
17
|
+
# wide unless told otherwise, so INTERVAL '100' DAY is an error. The
|
|
18
|
+
# functions that make an interval of a number take any size, one for
|
|
19
|
+
# the year-month intervals and one for the day-second.
|
|
20
|
+
def add_interval(date, amount, unit, subtract, _date_only)
|
|
21
|
+
interval = Arel::Nodes::NamedFunction.new(
|
|
22
|
+
unit == :year || unit == :month ? "NUMTOYMINTERVAL" : "NUMTODSINTERVAL",
|
|
23
|
+
[Arel::Nodes.build_quoted(amount), Arel::Nodes.build_quoted(unit.to_s.upcase)])
|
|
24
|
+
Arel::Nodes::Grouping.new(
|
|
25
|
+
Arel::Nodes::InfixOperation.new(subtract ? :- : :+, date, interval))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Oracle keeps scalars and structures in different functions: JSON_VALUE
|
|
29
|
+
# reads a scalar out as text, JSON_QUERY a fragment as JSON. dig keeps
|
|
30
|
+
# JSON, so it takes JSON_QUERY with 23ai's ALLOW SCALARS, which returns
|
|
31
|
+
# a scalar leaf as itself rather than wrapping or refusing it.
|
|
32
|
+
def json_path(document, dollar_path, _steps, json_value, model)
|
|
33
|
+
unless json_value
|
|
34
|
+
return Arel::Nodes::NamedFunction.new(
|
|
35
|
+
"JSON_VALUE", [document, Arel::Nodes.build_quoted(dollar_path)])
|
|
36
|
+
end
|
|
37
|
+
Arel.sql(
|
|
38
|
+
"JSON_QUERY(#{compile(document, model)}, " \
|
|
39
|
+
"#{quote(dollar_path, model)} RETURNING VARCHAR2(4000) ALLOW SCALARS " \
|
|
40
|
+
"NULL ON EMPTY)")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def json_has_key(document, _name, path, _model)
|
|
44
|
+
Arel::Nodes::NamedFunction.new("JSON_EXISTS", [document, path])
|
|
45
|
+
end
|
|
46
|
+
|
|
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
|
+
def json_argument(value, model)
|
|
55
|
+
Arel.sql("#{quote(JSON.generate(value), model)} FORMAT JSON")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# JSON_TRANSFORM is Oracle's one editing function; its SET and the value
|
|
59
|
+
# beside it are grammar, so the call is written out, RETURNING text that
|
|
60
|
+
# ruby-oci8 can fetch.
|
|
61
|
+
def json_set(document, _steps, dollar_path, value, expression, model)
|
|
62
|
+
model.with_connection do |connection|
|
|
63
|
+
set =
|
|
64
|
+
if expression
|
|
65
|
+
connection.visitor.compile(expression)
|
|
66
|
+
elsif json_document_value?(value)
|
|
67
|
+
"#{connection.quote(JSON.generate(value))} FORMAT JSON"
|
|
68
|
+
else
|
|
69
|
+
connection.quote(value)
|
|
70
|
+
end
|
|
71
|
+
Arel.sql(
|
|
72
|
+
"JSON_TRANSFORM(#{connection.visitor.compile(document)}, " \
|
|
73
|
+
"SET #{connection.quote(dollar_path)} = #{set} RETURNING VARCHAR2(4000))")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def json_remove(document, dollar_paths, _steps, model)
|
|
78
|
+
model.with_connection do |connection|
|
|
79
|
+
removes = dollar_paths.map { |path| "REMOVE #{connection.quote(path)}" }
|
|
80
|
+
Arel.sql(
|
|
81
|
+
"JSON_TRANSFORM(#{connection.visitor.compile(document)}, " \
|
|
82
|
+
"#{removes.join(', ')} RETURNING VARCHAR2(4000))")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Oracle pairs a key with its value by the VALUE keyword rather than by
|
|
87
|
+
# position, keeps a NULL only when told NULL ON NULL, and returns the
|
|
88
|
+
# native JSON type unless a RETURNING asks for text ruby-oci8 can fetch.
|
|
89
|
+
def json_build(kind, keys, args, model)
|
|
90
|
+
model.with_connection do |connection|
|
|
91
|
+
compiled = args.map { |arg| connection.visitor.compile(arg) }
|
|
92
|
+
body =
|
|
93
|
+
if kind == :array
|
|
94
|
+
compiled.join(", ")
|
|
95
|
+
else
|
|
96
|
+
keys.zip(compiled).map { |key, arg| "#{connection.quote(key)} VALUE #{arg}" }.join(", ")
|
|
97
|
+
end
|
|
98
|
+
Arel.sql("JSON_#{kind.to_s.upcase}(#{body} NULL ON NULL RETURNING VARCHAR2(4000))")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def check_json_aggregate_window(source, model)
|
|
103
|
+
raise NotImplementedError,
|
|
104
|
+
"#{source} over a window has no equivalent on " \
|
|
105
|
+
"#{model.connection_db_config.adapter}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# PostgreSQL, and the adapters that answer for the same server.
|
|
7
|
+
class Postgresql < Dialect
|
|
8
|
+
FUNCTIONS = { log2: nil, rand: "RANDOM" }.freeze
|
|
9
|
+
|
|
10
|
+
# PostgreSQL counts the bits of a bit string rather than a number, so
|
|
11
|
+
# the argument is cast, to bit(64) for a negative to come back as the
|
|
12
|
+
# MySQL family has it.
|
|
13
|
+
def bit_count(expr, _model)
|
|
14
|
+
AST::Function.new("BIT_COUNT", [AST::Cast.new(expr, "bit(64)")])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# PostgreSQL's XOR is #, where ^ is exponentiation.
|
|
18
|
+
def bitwise_xor(left, right)
|
|
19
|
+
Arel::Nodes::InfixOperation.new("#", left, right)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def json_path(document, _dollar_path, steps, json_value, _model)
|
|
23
|
+
Arel::Nodes::InfixOperation.new(
|
|
24
|
+
json_value ? :"#>" : :"#>>", document, Arel::Nodes.build_quoted(steps))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# An untyped JSON literal beside a jsonb operand coerces to jsonb, so
|
|
28
|
+
# it passes as it is.
|
|
29
|
+
def json_literal(json, _model)
|
|
30
|
+
json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def json_contains(document, json, _model)
|
|
34
|
+
Arel::Nodes::Contains.new(document, json)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The ? operator, which a GIN index matches where jsonb_exists never is.
|
|
38
|
+
def json_has_key(document, name, _path, _model)
|
|
39
|
+
Arel::Nodes::InfixOperation.new(:"?", document, name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def json_keys(document, model)
|
|
43
|
+
sql = compile(document, model)
|
|
44
|
+
Arel.sql("CASE WHEN jsonb_typeof(#{sql}) = 'object' " \
|
|
45
|
+
"THEN COALESCE((SELECT jsonb_agg(k) FROM jsonb_object_keys(#{sql}) k), " \
|
|
46
|
+
"CAST('[]' AS jsonb)) END")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# jsonb_set takes jsonb: an expression is turned into it, and a Ruby
|
|
50
|
+
# value goes in as the JSON that says it -- '"x"' rather than 'x'.
|
|
51
|
+
def json_set(document, steps, _dollar_path, value, expression, _model)
|
|
52
|
+
set = expression ? Arel::Nodes::NamedFunction.new("to_jsonb", [expression]) :
|
|
53
|
+
Arel::Nodes.build_quoted(JSON.generate(value))
|
|
54
|
+
Arel::Nodes::NamedFunction.new(
|
|
55
|
+
"jsonb_set", [document, Arel::Nodes.build_quoted(steps), set])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# jsonb subtracts an array of keys; an untyped array literal would be
|
|
59
|
+
# read as a single key, so it is cast to text[].
|
|
60
|
+
def json_remove(document, _dollar_paths, steps, _model)
|
|
61
|
+
keys = Arel::Nodes::NamedFunction.new(
|
|
62
|
+
"CAST", [Arel::Nodes::As.new(
|
|
63
|
+
Arel::Nodes.build_quoted(steps), Arel::Nodes::SqlLiteral.new("text[]"))])
|
|
64
|
+
# Grouped because - binds tighter than #>.
|
|
65
|
+
Arel::Nodes::InfixOperation.new(:-, Arel::Nodes::Grouping.new(document), keys)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def json_build(kind, keys, args, _model)
|
|
69
|
+
Arel::Nodes::NamedFunction.new(
|
|
70
|
+
kind == :array ? "jsonb_build_array" : "jsonb_build_object",
|
|
71
|
+
json_build_body(kind, keys, args))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# jsonb_build_* takes typed arguments, so a JSON literal is cast; left
|
|
75
|
+
# untyped it would be text, and land as a string.
|
|
76
|
+
def json_build_argument(value, _model)
|
|
77
|
+
Arel::Nodes::NamedFunction.new(
|
|
78
|
+
"CAST", [Arel::Nodes::As.new(
|
|
79
|
+
Arel::Nodes.build_quoted(JSON.generate(value)),
|
|
80
|
+
Arel::Nodes::SqlLiteral.new("jsonb"))])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def json_aggregate_name(kind)
|
|
84
|
+
kind == :arrayagg ? "jsonb_agg" : "jsonb_object_agg"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# STRING_AGG takes text and nothing else -- over an integer column it
|
|
88
|
+
# is a function that does not exist -- so the operand is cast unless
|
|
89
|
+
# the model says it is a string already.
|
|
90
|
+
def string_agg(operand, separator, orders, string, model)
|
|
91
|
+
unless string
|
|
92
|
+
operand = Arel::Nodes::NamedFunction.new(
|
|
93
|
+
"CAST", [Arel::Nodes::As.new(operand, Arel::Nodes::SqlLiteral.new("text"))])
|
|
94
|
+
end
|
|
95
|
+
string_agg_call("STRING_AGG", operand, separator, orders, model)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def grouping_supported?(_kind) = true
|
|
99
|
+
|
|
100
|
+
# The names PostgreSQL knows, letters and digits and the _ . - its
|
|
101
|
+
# catalog spells them with. Wider than the bare families' plain
|
|
102
|
+
# identifier because the name is quoted, so a hyphen is safe -- and
|
|
103
|
+
# every ICU collation, en-US-x-icu among them, is spelled with one.
|
|
104
|
+
# @private
|
|
105
|
+
COLLATION_NAME = /\A[[:alnum:]_.-]+\z/
|
|
106
|
+
|
|
107
|
+
# PostgreSQL's own collation names are case-sensitive and upper, "C",
|
|
108
|
+
# "POSIX", so the name is quoted as an identifier to keep it from
|
|
109
|
+
# folding to lower case. The quoter is asked to spell it, as
|
|
110
|
+
# excluded's VALUES(column) is on MySQL.
|
|
111
|
+
def collate(operand, name, model)
|
|
112
|
+
AST.check_name(name, COLLATION_NAME, "collation name")
|
|
113
|
+
quoted = model.with_connection { |connection| connection.quote_column_name(name) }
|
|
114
|
+
Arel::Nodes::InfixOperation.new(
|
|
115
|
+
"COLLATE", operand, Arel::Nodes::SqlLiteral.new(quoted))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# Microsoft SQL Server, reached through the sqlserver adapter over
|
|
7
|
+
# tiny_tds. Loaded only when a query is built for it.
|
|
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.
|
|
13
|
+
FUNCTIONS = {
|
|
14
|
+
char_length: "LEN",
|
|
15
|
+
format: nil, rand: nil, date_trunc: nil,
|
|
16
|
+
bit_and: nil, bit_or: nil, bit_xor: nil,
|
|
17
|
+
current_date: nil, current_time: nil, localtime: nil, localtimestamp: nil,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# No FILTER clause, so the aggregate node builds the CASE instead.
|
|
21
|
+
def filter_supported? = false
|
|
22
|
+
|
|
23
|
+
# No EXTRACT (it has DATEPART), and its datetime value functions take
|
|
24
|
+
# no precision.
|
|
25
|
+
def extract_supported? = false
|
|
26
|
+
def datetime_precision_supported? = false
|
|
27
|
+
|
|
28
|
+
# No boolean type: the column is a bit. ISNULL reads a NULL as the
|
|
29
|
+
# value the test is not looking for -- 0 for true?, 1 for false? --
|
|
30
|
+
# so the plain form drops it and, being never NULL itself, the
|
|
31
|
+
# negation is exactly NOT of the plain form, matching what ! builds.
|
|
32
|
+
def truth_value(operand, value, negated, _model)
|
|
33
|
+
equals = Arel::Nodes::Equality.new(
|
|
34
|
+
Arel::Nodes::NamedFunction.new("ISNULL", [operand, value ? 0 : 1]),
|
|
35
|
+
value ? 1 : 0)
|
|
36
|
+
negated ? Arel::Nodes::Not.new(equals) : equals
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# DATEADD(day, 3, x), the unit a bare keyword; a subtraction is a
|
|
40
|
+
# negative amount, there being no DATESUB.
|
|
41
|
+
def add_interval(date, amount, unit, subtract, _date_only)
|
|
42
|
+
Arel::Nodes::NamedFunction.new(
|
|
43
|
+
"DATEADD",
|
|
44
|
+
[Arel::Nodes::SqlLiteral.new(unit.to_s),
|
|
45
|
+
Arel::Nodes.build_quoted(subtract ? -amount : amount), date])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# STRING_AGG, with the WITHIN GROUP only when there is an order to
|
|
49
|
+
# put in it.
|
|
50
|
+
def string_agg(operand, separator, orders, _string, model)
|
|
51
|
+
call = Arel::Nodes::NamedFunction.new(
|
|
52
|
+
"STRING_AGG", [operand, Arel::Nodes.build_quoted(separator)])
|
|
53
|
+
return call if orders.empty?
|
|
54
|
+
Arel.sql("#{compile(call, model)} WITHIN GROUP " \
|
|
55
|
+
"(ORDER BY #{compile_list(orders, model)})")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_string_aggregate_window(model)
|
|
59
|
+
raise NotImplementedError,
|
|
60
|
+
"string_agg over a window has no equivalent on #{model.connection_db_config.adapter}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# dig_text reads a scalar out with JSON_VALUE; dig keeps JSON with
|
|
64
|
+
# JSON_QUERY, which returns a fragment and NULL for a scalar leaf.
|
|
65
|
+
def json_path(document, dollar_path, _steps, json_value, _model)
|
|
66
|
+
Arel::Nodes::NamedFunction.new(
|
|
67
|
+
json_value ? "JSON_QUERY" : "JSON_VALUE",
|
|
68
|
+
[document, Arel::Nodes.build_quoted(dollar_path)])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# JSON_PATH_EXISTS returns a bit, which the condition compares to 1.
|
|
72
|
+
def json_has_key(document, _name, path, _model)
|
|
73
|
+
Arel::Nodes::Equality.new(
|
|
74
|
+
Arel::Nodes::NamedFunction.new("JSON_PATH_EXISTS", [document, path]),
|
|
75
|
+
Arel::Nodes.build_quoted(1))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A document, a boolean or a bare scalar written where JSON is wanted
|
|
79
|
+
# rides in through JSON_QUERY as the JSON it spells.
|
|
80
|
+
def json_argument(value, _model)
|
|
81
|
+
Arel::Nodes::NamedFunction.new(
|
|
82
|
+
"JSON_QUERY", [Arel::Nodes.build_quoted(JSON.generate(value))])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# bury: JSON_MODIFY sets a value at a path.
|
|
86
|
+
def json_set(document, _steps, dollar_path, value, expression, model)
|
|
87
|
+
Arel::Nodes::NamedFunction.new(
|
|
88
|
+
"JSON_MODIFY",
|
|
89
|
+
[document, Arel::Nodes.build_quoted(dollar_path),
|
|
90
|
+
json_modify_value(value, expression, model)])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# except: JSON_MODIFY deletes a key when it sets it to a literal NULL,
|
|
94
|
+
# one nested call per key.
|
|
95
|
+
def json_remove(document, dollar_paths, _steps, _model)
|
|
96
|
+
dollar_paths.reduce(document) do |doc, path|
|
|
97
|
+
Arel::Nodes::NamedFunction.new(
|
|
98
|
+
"JSON_MODIFY",
|
|
99
|
+
[doc, Arel::Nodes.build_quoted(path), Arel::Nodes::SqlLiteral.new("NULL")])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
# A document or array goes in through JSON_QUERY, which wants an
|
|
105
|
+
# object or an array and refuses a scalar; a bare scalar is quoted.
|
|
106
|
+
# A boolean, a null or a dug scalar is bury's business the tests skip.
|
|
107
|
+
def json_modify_value(value, expression, model)
|
|
108
|
+
return expression if expression
|
|
109
|
+
return json_argument(value, model) if value.is_a?(::Hash) || value.is_a?(::Array)
|
|
110
|
+
Arel::Nodes.build_quoted(value)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Refined
|
|
5
|
+
class Dialect
|
|
6
|
+
# SQLite: the fewest of the extras, and a JSON path through its own
|
|
7
|
+
# operators.
|
|
8
|
+
class Sqlite < Dialect
|
|
9
|
+
FUNCTIONS = {
|
|
10
|
+
char_length: "LENGTH", greatest: "MAX", least: "MIN",
|
|
11
|
+
now: nil, date_trunc: nil, rand: "RANDOM",
|
|
12
|
+
bit_and: nil, bit_or: nil, bit_xor: nil,
|
|
13
|
+
localtime: nil, localtimestamp: nil,
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def datetime_precision_supported? = false
|
|
17
|
+
def extract_supported? = false
|
|
18
|
+
def quantifiers_supported? = false
|
|
19
|
+
|
|
20
|
+
def check_lateral(model)
|
|
21
|
+
raise NotImplementedError,
|
|
22
|
+
"a lateral join has no equivalent on #{model.connection_db_config.adapter}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A date is moved by a modifier to date() or datetime(), '+3 day';
|
|
26
|
+
# the unit's own name reads there, singular. datetime() answers with a
|
|
27
|
+
# time of day whatever it is given, so a date column, as the node
|
|
28
|
+
# tells one, goes through date() and stays a date.
|
|
29
|
+
def add_interval(date, amount, unit, subtract, date_only)
|
|
30
|
+
modifier = format("%+d %s", subtract ? -amount : amount, unit)
|
|
31
|
+
Arel::Nodes::NamedFunction.new(
|
|
32
|
+
date_only ? "date" : "datetime", [date, Arel::Nodes.build_quoted(modifier)])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def json_keys(document, model)
|
|
36
|
+
sql = compile(document, model)
|
|
37
|
+
Arel.sql("CASE WHEN json_type(#{sql}) = 'object' " \
|
|
38
|
+
"THEN (SELECT json_group_array(key) FROM json_each(#{sql})) END")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def json_argument(value, _model)
|
|
42
|
+
Arel::Nodes::NamedFunction.new(
|
|
43
|
+
"json", [Arel::Nodes.build_quoted(JSON.generate(value))])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def json_aggregate_name(kind)
|
|
47
|
+
kind == :arrayagg ? "json_group_array" : "json_group_object"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# group_concat, with the ORDER BY inside the call from 3.44 on.
|
|
51
|
+
def string_agg(operand, separator, orders, _string, model)
|
|
52
|
+
string_agg_call("group_concat", operand, separator, orders, model)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|