active_record_extended_telescope 2.0.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 +7 -0
- data/README.md +870 -0
- data/lib/active_record_extended.rb +10 -0
- data/lib/active_record_extended/active_record.rb +25 -0
- data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
- data/lib/active_record_extended/arel.rb +7 -0
- data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
- data/lib/active_record_extended/arel/nodes.rb +49 -0
- data/lib/active_record_extended/arel/predications.rb +50 -0
- data/lib/active_record_extended/arel/sql_literal.rb +16 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
- data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
- data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
- data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
- data/lib/active_record_extended/query_methods/any_of.rb +93 -0
- data/lib/active_record_extended/query_methods/either.rb +62 -0
- data/lib/active_record_extended/query_methods/inet.rb +88 -0
- data/lib/active_record_extended/query_methods/json.rb +329 -0
- data/lib/active_record_extended/query_methods/select.rb +118 -0
- data/lib/active_record_extended/query_methods/unionize.rb +249 -0
- data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
- data/lib/active_record_extended/query_methods/window.rb +93 -0
- data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
- data/lib/active_record_extended/utilities/order_by.rb +77 -0
- data/lib/active_record_extended/utilities/support.rb +178 -0
- data/lib/active_record_extended/version.rb +5 -0
- data/lib/active_record_extended_telescope.rb +4 -0
- data/spec/active_record_extended_spec.rb +7 -0
- data/spec/query_methods/any_of_spec.rb +131 -0
- data/spec/query_methods/array_query_spec.rb +64 -0
- data/spec/query_methods/either_spec.rb +59 -0
- data/spec/query_methods/hash_query_spec.rb +45 -0
- data/spec/query_methods/inet_query_spec.rb +112 -0
- data/spec/query_methods/json_spec.rb +157 -0
- data/spec/query_methods/select_spec.rb +115 -0
- data/spec/query_methods/unionize_spec.rb +165 -0
- data/spec/query_methods/window_spec.rb +51 -0
- data/spec/query_methods/with_cte_spec.rb +50 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
- data/spec/sql_inspections/arel/array_spec.rb +63 -0
- data/spec/sql_inspections/arel/inet_spec.rb +66 -0
- data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
- data/spec/sql_inspections/either_sql_spec.rb +55 -0
- data/spec/sql_inspections/json_sql_spec.rb +82 -0
- data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
- data/spec/sql_inspections/window_sql_spec.rb +98 -0
- data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
- data/spec/support/database_cleaner.rb +15 -0
- data/spec/support/models.rb +68 -0
- metadata +245 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ar_outer_joins"
|
4
|
+
|
5
|
+
module ActiveRecordExtended
|
6
|
+
module QueryMethods
|
7
|
+
module Either
|
8
|
+
XOR_FIELD_SQL = "(CASE WHEN %<t1>s.%<c1>s IS NULL THEN %<t2>s.%<c2>s ELSE %<t1>s.%<c1>s END) "
|
9
|
+
XOR_FIELD_KEYS = [:t1, :c1, :t2, :c2].freeze
|
10
|
+
|
11
|
+
def either_join(initial_association, fallback_association)
|
12
|
+
associations = [initial_association, fallback_association]
|
13
|
+
association_options = xor_field_options_for_associations(associations)
|
14
|
+
condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
|
15
|
+
outer_joins(associations).where(Arel.sql(condition__query))
|
16
|
+
end
|
17
|
+
alias either_joins either_join
|
18
|
+
|
19
|
+
def either_order(direction, **associations_and_columns)
|
20
|
+
reflected_columns = map_columns_to_tables(associations_and_columns)
|
21
|
+
conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
|
22
|
+
outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
|
23
|
+
end
|
24
|
+
alias either_orders either_order
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def xor_field_sql(options)
|
29
|
+
XOR_FIELD_SQL % Hash[xor_field_options(options)]
|
30
|
+
end
|
31
|
+
|
32
|
+
def sort_order_sql(dir)
|
33
|
+
["asc", "desc"].include?(dir.to_s) ? dir.to_s : "asc"
|
34
|
+
end
|
35
|
+
|
36
|
+
def xor_field_options(options)
|
37
|
+
str_args = options.flatten.take(XOR_FIELD_KEYS.size).map(&:to_s)
|
38
|
+
Hash[XOR_FIELD_KEYS.zip(str_args)]
|
39
|
+
end
|
40
|
+
|
41
|
+
def map_columns_to_tables(associations_and_columns)
|
42
|
+
if associations_and_columns.respond_to?(:transform_keys)
|
43
|
+
associations_and_columns.transform_keys { |assc| reflect_on_association(assc).table_name }
|
44
|
+
else
|
45
|
+
associations_and_columns.each_with_object({}) do |(assc, value), key_table|
|
46
|
+
reflect_table = reflect_on_association(assc).table_name
|
47
|
+
key_table[reflect_table] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def xor_field_options_for_associations(associations)
|
53
|
+
associations.each_with_object({}) do |association_name, options|
|
54
|
+
reflection = reflect_on_association(association_name)
|
55
|
+
options[reflection.table_name] = reflection.foreign_key
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveRecord::Base.extend(ActiveRecordExtended::QueryMethods::Either)
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Inet
|
6
|
+
# Finds matching inet column records that fall within a given submasked IP range
|
7
|
+
#
|
8
|
+
# Column(inet) << "127.0.0.1/24"
|
9
|
+
#
|
10
|
+
# User.where.inet_contained_within(ip: "127.0.0.1/16")
|
11
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" << '127.0.0.1/16'"
|
12
|
+
#
|
13
|
+
# User.where.inet_contained_within(ip: IPAddr.new("192.168.2.0/24"))
|
14
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" << '192.168.2.0/24'"
|
15
|
+
#
|
16
|
+
def inet_contained_within(opts, *rest)
|
17
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainedWithin, "inet_contained_within")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Finds matching inet column records that fall within a given submasked IP range and also finds records that also
|
21
|
+
# contain a submasking field that fall within range too.
|
22
|
+
#
|
23
|
+
# Column(inet) <<= "127.0.0.1/24"
|
24
|
+
#
|
25
|
+
# User.where.inet_contained_within_or_equals(ip: "127.0.0.1/16")
|
26
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" <<= '127.0.0.44/32'"
|
27
|
+
#
|
28
|
+
# User.where.inet_contained_within_or_equals(ip: IPAddr.new("192.168.2.0/24"))
|
29
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" <<= '192.168.2.0/24'"
|
30
|
+
#
|
31
|
+
def inet_contained_within_or_equals(opts, *rest)
|
32
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainedWithinEquals, "inet_contained_within_or_equals")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Finds records that contain a submask and the supplied IP address falls within its range.
|
36
|
+
#
|
37
|
+
# Column(inet) >>= "127.0.0.1/24"
|
38
|
+
#
|
39
|
+
# User.where.inet_contained_within_or_equals(ip: "127.0.255.255")
|
40
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >>= '127.0.255.255'"
|
41
|
+
#
|
42
|
+
# User.where.inet_contained_within_or_equals(ip: IPAddr.new("127.0.0.255"))
|
43
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >>= '127.0.0.255/32'"
|
44
|
+
#
|
45
|
+
def inet_contains_or_equals(opts, *rest)
|
46
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Inet::ContainsEquals, "inet_contains_or_equals")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Strictly finds records that contain a submask and the supplied IP address falls within its range.
|
50
|
+
#
|
51
|
+
# Column(inet) >>= "127.0.0.1"
|
52
|
+
#
|
53
|
+
# User.where.inet_contained_within_or_equals(ip: "127.0.255.255")
|
54
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >> '127.0.255.255'"
|
55
|
+
#
|
56
|
+
# User.where.inet_contained_within_or_equals(ip: IPAddr.new("127.0.0.255"))
|
57
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >> '127.0.0.255/32'"
|
58
|
+
#
|
59
|
+
def inet_contains(opts, *rest)
|
60
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Inet::Contains, "inet_contains")
|
61
|
+
end
|
62
|
+
|
63
|
+
# This method is a combination of `inet_contains` and `inet_contained_within`
|
64
|
+
#
|
65
|
+
# Finds records that are contained within a given submask. And will also find records where their submask is also
|
66
|
+
# contains a given IP or IP submask.
|
67
|
+
#
|
68
|
+
# Column(inet) && "127.0.0.1/28"
|
69
|
+
#
|
70
|
+
# User.where.inet_contains_or_is_contained_by(ip: "127.0.255.255/28")
|
71
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" && '127.0.255.255/28'"
|
72
|
+
#
|
73
|
+
# User.where.inet_contains_or_is_contained_by(ip: IPAddr.new("127.0.0.255"))
|
74
|
+
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" && '127.0.0.255/32'"
|
75
|
+
#
|
76
|
+
def inet_contains_or_is_contained_within(opts, *rest)
|
77
|
+
substitute_comparisons(
|
78
|
+
opts,
|
79
|
+
rest,
|
80
|
+
Arel::Nodes::Inet::ContainsOrContainedWithin,
|
81
|
+
"inet_contains_or_is_contained_within"
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
ActiveRecord::QueryMethods::WhereChain.prepend(ActiveRecordExtended::QueryMethods::Inet)
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Json
|
6
|
+
JSON_QUERY_METHODS = [
|
7
|
+
:select_row_to_json,
|
8
|
+
:json_build_object,
|
9
|
+
:jsonb_build_object,
|
10
|
+
:json_build_literal,
|
11
|
+
:jsonb_build_literal
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
class JsonChain
|
15
|
+
include ::ActiveRecordExtended::Utilities::Support
|
16
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
17
|
+
|
18
|
+
DEFAULT_ALIAS = '"results"'
|
19
|
+
TO_JSONB_OPTIONS = [:array_agg, :distinct, :to_jsonb].to_set.freeze
|
20
|
+
ARRAY_OPTIONS = [:array, true].freeze
|
21
|
+
|
22
|
+
def initialize(scope)
|
23
|
+
@scope = scope
|
24
|
+
end
|
25
|
+
|
26
|
+
def row_to_json!(**args, &block)
|
27
|
+
options = json_object_options(args, except: [:values, :value])
|
28
|
+
build_row_to_json(**options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def json_build_object!(*args)
|
32
|
+
options = json_object_options(args, except: [:values, :cast_with, :order_by])
|
33
|
+
build_json_object(Arel::Nodes::JsonBuildObject, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def jsonb_build_object!(*args)
|
37
|
+
options = json_object_options(args, except: [:values, :cast_with, :order_by])
|
38
|
+
build_json_object(Arel::Nodes::JsonbBuildObject, **options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def json_build_literal!(*args)
|
42
|
+
options = json_object_options(args, only: [:values, :col_alias])
|
43
|
+
build_json_literal(Arel::Nodes::JsonBuildObject, **options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def jsonb_build_literal!(*args)
|
47
|
+
options = json_object_options(args, only: [:values, :col_alias])
|
48
|
+
build_json_literal(Arel::Nodes::JsonbBuildObject, **options)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def build_json_literal(arel_klass, values:, col_alias: DEFAULT_ALIAS)
|
54
|
+
json_values = flatten_to_sql(values.to_a) { |value| literal_key(value) }
|
55
|
+
col_alias = double_quote(col_alias)
|
56
|
+
json_build_obj = arel_klass.new(json_values)
|
57
|
+
@scope.select(nested_alias_escape(json_build_obj, col_alias))
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_json_object(arel_klass, from:, key: key_generator, value: nil, col_alias: DEFAULT_ALIAS)
|
61
|
+
tbl_alias = double_quote(key)
|
62
|
+
col_alias = double_quote(col_alias)
|
63
|
+
col_key = literal_key(key)
|
64
|
+
col_value = to_arel_sql(value.presence || tbl_alias)
|
65
|
+
json_build_object = arel_klass.new(to_sql_array(col_key, col_value))
|
66
|
+
|
67
|
+
unless /".+"/.match?(col_value)
|
68
|
+
warn("`#{col_value}`: the `value` argument should contain a double quoted key reference for safety")
|
69
|
+
end
|
70
|
+
|
71
|
+
@scope.select(nested_alias_escape(json_build_object, col_alias)).from(nested_alias_escape(from, tbl_alias))
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_row_to_json(from:, **options, &block)
|
75
|
+
key = options[:key]
|
76
|
+
row_to_json = ::Arel::Nodes::RowToJson.new(double_quote(key))
|
77
|
+
row_to_json = ::Arel::Nodes::ToJsonb.new(row_to_json) if options.dig(:cast_with, :to_jsonb)
|
78
|
+
|
79
|
+
dummy_table = from_clause_constructor(from, key).select(row_to_json)
|
80
|
+
dummy_table = dummy_table.instance_eval(&block) if block
|
81
|
+
return dummy_table if options[:col_alias].blank?
|
82
|
+
|
83
|
+
query = wrap_row_to_json(dummy_table, options)
|
84
|
+
@scope.select(query)
|
85
|
+
end
|
86
|
+
|
87
|
+
def wrap_row_to_json(dummy_table, options)
|
88
|
+
cast_opts = options[:cast_with]
|
89
|
+
col_alias = options[:col_alias]
|
90
|
+
order_by = options[:order_by]
|
91
|
+
|
92
|
+
if cast_opts[:array_agg] || cast_opts[:distinct]
|
93
|
+
wrap_with_agg_array(dummy_table, col_alias, order_by: order_by, distinct: cast_opts[:distinct])
|
94
|
+
elsif cast_opts[:array]
|
95
|
+
wrap_with_array(dummy_table, col_alias, order_by: order_by)
|
96
|
+
else
|
97
|
+
nested_alias_escape(dummy_table, col_alias)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def json_object_options(args, except: [], only: []) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
102
|
+
options = {}
|
103
|
+
lean_opts = lambda do |key, &block|
|
104
|
+
if only.present?
|
105
|
+
options[key] ||= block.call if only.include?(key)
|
106
|
+
elsif !except.include?(key)
|
107
|
+
options[key] ||= block.call
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
flatten_safely(args) do |arg|
|
112
|
+
next if arg.nil?
|
113
|
+
|
114
|
+
if arg.is_a?(Hash)
|
115
|
+
lean_opts.call(:key) { arg.fetch(:key, key_generator) }
|
116
|
+
lean_opts.call(:value) { arg[:value].presence }
|
117
|
+
lean_opts.call(:col_alias) { arg[:as] }
|
118
|
+
lean_opts.call(:order_by) { order_by_expression(arg[:order_by]) }
|
119
|
+
lean_opts.call(:from) { arg[:from].tap { |from_clause| pipe_cte_with!(from_clause) } }
|
120
|
+
lean_opts.call(:cast_with) { casting_options(arg[:cast_with]) }
|
121
|
+
end
|
122
|
+
|
123
|
+
unless except.include?(:values)
|
124
|
+
options[:values] ||= []
|
125
|
+
options[:values] << (arg.respond_to?(:to_a) ? arg.to_a : arg)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
options.tap(&:compact!)
|
130
|
+
end
|
131
|
+
|
132
|
+
def casting_options(cast_with)
|
133
|
+
return {} if cast_with.blank?
|
134
|
+
|
135
|
+
skip_convert = [Symbol, TrueClass, FalseClass]
|
136
|
+
Array(cast_with).compact.each_with_object({}) do |arg, options|
|
137
|
+
arg = arg.to_sym unless skip_convert.include?(arg.class)
|
138
|
+
options[:to_jsonb] |= TO_JSONB_OPTIONS.include?(arg)
|
139
|
+
options[:array] |= ARRAY_OPTIONS.include?(arg)
|
140
|
+
options[:array_agg] |= arg == :array_agg
|
141
|
+
options[:distinct] |= arg == :distinct
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Appends a select statement that contains a subquery that is converted to a json response
|
147
|
+
#
|
148
|
+
# Arguments:
|
149
|
+
# - from: [String, Arel, or ActiveRecord::Relation] A subquery that can be nested into a ROW_TO_JSON clause
|
150
|
+
#
|
151
|
+
# Options:
|
152
|
+
# - as: [Symbol or String] (default="results"): What the column will be aliased to
|
153
|
+
#
|
154
|
+
# - key: [Symbol or String] (default=[random letter]) What the row clause will be set as.
|
155
|
+
# - This is useful if you would like to add additional mid-level clauses (see mid-level scope example)
|
156
|
+
#
|
157
|
+
# - cast_with [Symbol or Array of symbols]: Actions to transform your query
|
158
|
+
# * :to_jsonb
|
159
|
+
# * :array
|
160
|
+
# * :array_agg (including just :array with this option will favor :array_agg)
|
161
|
+
# * :distinct (auto applies :array_agg & :to_jsonb)
|
162
|
+
#
|
163
|
+
# - order_by [Symbol or hash]: Applies an ordering operation (similar to ActiveRecord #order)
|
164
|
+
# - NOTE: this option will be ignored if you need to order a DISTINCT Aggregated Array,
|
165
|
+
# since postgres will thrown an error.
|
166
|
+
#
|
167
|
+
#
|
168
|
+
#
|
169
|
+
# Examples:
|
170
|
+
# subquery = Group.select(:name, :category_id).where("user_id = users.id")
|
171
|
+
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
|
172
|
+
# #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
|
173
|
+
#
|
174
|
+
# - Adding mid-level scopes:
|
175
|
+
#
|
176
|
+
# subquery = Group.select(:name, :category_id)
|
177
|
+
# User.select_row_to_json(subquery, key: :group, cast_with: :array) do |scope|
|
178
|
+
# scope.where(group: { name: "Nerd Core" })
|
179
|
+
# end
|
180
|
+
# #=> ```sql
|
181
|
+
# SELECT ARRAY(
|
182
|
+
# SELECT ROW_TO_JSON("group")
|
183
|
+
# FROM(SELECT name, category_id FROM groups) AS group
|
184
|
+
# WHERE group.name = 'Nerd Core'
|
185
|
+
# )
|
186
|
+
# ```
|
187
|
+
#
|
188
|
+
#
|
189
|
+
# - Array of JSONB objects
|
190
|
+
#
|
191
|
+
# subquery = Group.select(:name, :category_id)
|
192
|
+
# User.select_row_to_json(subquery, key: :group, cast_with: [:array, :to_jsonb]) do |scope|
|
193
|
+
# scope.where(group: { name: "Nerd Core" })
|
194
|
+
# end
|
195
|
+
# #=> ```sql
|
196
|
+
# SELECT ARRAY(
|
197
|
+
# SELECT TO_JSONB(ROW_TO_JSON("group"))
|
198
|
+
# FROM(SELECT name, category_id FROM groups) AS group
|
199
|
+
# WHERE group.name = 'Nerd Core'
|
200
|
+
# )
|
201
|
+
# ```
|
202
|
+
#
|
203
|
+
# - Distinct Aggregated Array
|
204
|
+
#
|
205
|
+
# subquery = Group.select(:name, :category_id)
|
206
|
+
# User.select_row_to_json(subquery, key: :group, cast_with: [:array_agg, :distinct]) do |scope|
|
207
|
+
# scope.where(group: { name: "Nerd Core" })
|
208
|
+
# end
|
209
|
+
# #=> ```sql
|
210
|
+
# SELECT ARRAY_AGG(DISTINCT (
|
211
|
+
# SELECT TO_JSONB(ROW_TO_JSON("group"))
|
212
|
+
# FROM(SELECT name, category_id FROM groups) AS group
|
213
|
+
# WHERE group.name = 'Nerd Core'
|
214
|
+
# ))
|
215
|
+
# ```
|
216
|
+
#
|
217
|
+
# - Ordering a Non-aggregated Array
|
218
|
+
#
|
219
|
+
# subquery = Group.select(:name, :category_id)
|
220
|
+
# User.select_row_to_json(subquery, key: :group, cast_with: :array, order_by: { group: { name: :desc } })
|
221
|
+
# #=> ```sql
|
222
|
+
# SELECT ARRAY(
|
223
|
+
# SELECT ROW_TO_JSON("group")
|
224
|
+
# FROM(SELECT name, category_id FROM groups) AS group
|
225
|
+
# ORDER BY group.name DESC
|
226
|
+
# )
|
227
|
+
# ```
|
228
|
+
#
|
229
|
+
# - Ordering an Aggregated Array
|
230
|
+
#
|
231
|
+
# Subquery = Group.select(:name, :category_id)
|
232
|
+
# User
|
233
|
+
# .joins(:people_groups)
|
234
|
+
# .select_row_to_json(
|
235
|
+
# subquery,
|
236
|
+
# key: :group,
|
237
|
+
# cast_with: :array_agg,
|
238
|
+
# order_by: { people_groups: :category_id }
|
239
|
+
# )
|
240
|
+
# #=> ```sql
|
241
|
+
# SELECT ARRAY_AGG((
|
242
|
+
# SELECT ROW_TO_JSON("group")
|
243
|
+
# FROM(SELECT name, category_id FROM groups) AS group
|
244
|
+
# ORDER BY group.name DESC
|
245
|
+
# ) ORDER BY people_groups.category_id ASC)
|
246
|
+
# ```
|
247
|
+
#
|
248
|
+
def select_row_to_json(from = nil, **options, &block)
|
249
|
+
from.is_a?(Hash) ? options.merge!(from) : options.reverse_merge!(from: from)
|
250
|
+
options.compact!
|
251
|
+
raise ArgumentError.new("Required to provide a non-nilled from clause") unless options.key?(:from)
|
252
|
+
|
253
|
+
JsonChain.new(spawn).row_to_json!(**options, &block)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Creates a json response object that will convert all subquery results into a json compatible response
|
257
|
+
#
|
258
|
+
# Arguments:
|
259
|
+
# key: [Symbol or String]: What should this response return as
|
260
|
+
# from: [String, Arel, or ActiveRecord::Relation] : A subquery that can be nested into the top-level from clause
|
261
|
+
#
|
262
|
+
# Options:
|
263
|
+
# - as: [Symbol or String] (default="results"): What the column will be aliased to
|
264
|
+
#
|
265
|
+
#
|
266
|
+
# - value: [Symbol or String] (defaults=[key]): How the response should handel the json value return
|
267
|
+
#
|
268
|
+
# Example:
|
269
|
+
#
|
270
|
+
# - Generic example:
|
271
|
+
#
|
272
|
+
# subquery = Group.select(:name, :category_id).where("user_id = users.id")
|
273
|
+
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
|
274
|
+
# #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
|
275
|
+
#
|
276
|
+
# - Setting a custom value:
|
277
|
+
#
|
278
|
+
# Before:
|
279
|
+
# subquery = User.select(:name).where(id: 100..110).group(:name)
|
280
|
+
# User.build_json_object(:gang_members, subquery).take.results["gang_members"] #=> nil
|
281
|
+
#
|
282
|
+
# After:
|
283
|
+
# User.build_json_object(:gang_members, subquery, value: "COALESCE(array_agg(\"gang_members\"), 'BANG!')")
|
284
|
+
# .take
|
285
|
+
# .results["gang_members"] #=> "BANG!"
|
286
|
+
#
|
287
|
+
def json_build_object(key, from, **options)
|
288
|
+
options[:key] = key
|
289
|
+
options[:from] = from
|
290
|
+
JsonChain.new(spawn).json_build_object!(options)
|
291
|
+
end
|
292
|
+
|
293
|
+
def jsonb_build_object(key, from, **options)
|
294
|
+
options[:key] = key
|
295
|
+
options[:from] = from
|
296
|
+
JsonChain.new(spawn).jsonb_build_object!(options)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Appends a hash literal to the calling relations response
|
300
|
+
#
|
301
|
+
# Arguments: Requires an Array or Hash set of values
|
302
|
+
#
|
303
|
+
# Options:
|
304
|
+
#
|
305
|
+
# - as: [Symbol or String] (default="results"): What the column will be aliased to
|
306
|
+
#
|
307
|
+
# Example:
|
308
|
+
# - Supplying inputs as a Hash
|
309
|
+
# query = User.json_build_literal(number: 1, last_name: "json", pi: 3.14)
|
310
|
+
# query.take.results #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
|
311
|
+
#
|
312
|
+
# - Supplying inputs as an Array
|
313
|
+
#
|
314
|
+
# query = User.json_build_literal(:number, 1, :last_name, "json", :pi, 3.14)
|
315
|
+
# query.take.results #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
|
316
|
+
#
|
317
|
+
|
318
|
+
def json_build_literal(*args)
|
319
|
+
JsonChain.new(spawn).json_build_literal!(args)
|
320
|
+
end
|
321
|
+
|
322
|
+
def jsonb_build_literal(*args)
|
323
|
+
JsonChain.new(spawn).jsonb_build_literal!(args)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Json)
|