active_record_extended 1.1.0 → 2.0.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/README.md +87 -15
- data/lib/active_record_extended.rb +2 -1
- data/lib/active_record_extended/active_record.rb +2 -9
- data/lib/active_record_extended/active_record/relation_patch.rb +21 -4
- data/lib/active_record_extended/arel.rb +2 -0
- data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
- data/lib/active_record_extended/arel/nodes.rb +32 -41
- data/lib/active_record_extended/arel/predications.rb +4 -1
- data/lib/active_record_extended/arel/sql_literal.rb +16 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
- data/lib/active_record_extended/query_methods/any_of.rb +10 -8
- data/lib/active_record_extended/query_methods/either.rb +1 -1
- data/lib/active_record_extended/query_methods/inet.rb +7 -3
- data/lib/active_record_extended/query_methods/json.rb +156 -50
- data/lib/active_record_extended/query_methods/select.rb +118 -0
- data/lib/active_record_extended/query_methods/unionize.rb +14 -43
- data/lib/active_record_extended/query_methods/where_chain.rb +14 -6
- data/lib/active_record_extended/query_methods/window.rb +93 -0
- data/lib/active_record_extended/query_methods/with_cte.rb +102 -35
- 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 +1 -1
- data/spec/query_methods/any_of_spec.rb +40 -40
- data/spec/query_methods/array_query_spec.rb +14 -14
- data/spec/query_methods/either_spec.rb +14 -14
- data/spec/query_methods/hash_query_spec.rb +11 -11
- data/spec/query_methods/inet_query_spec.rb +33 -31
- data/spec/query_methods/json_spec.rb +42 -27
- data/spec/query_methods/select_spec.rb +115 -0
- data/spec/query_methods/unionize_spec.rb +56 -56
- data/spec/query_methods/window_spec.rb +51 -0
- data/spec/query_methods/with_cte_spec.rb +22 -12
- data/spec/spec_helper.rb +1 -1
- data/spec/sql_inspections/any_of_sql_spec.rb +12 -12
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
- data/spec/sql_inspections/arel/array_spec.rb +7 -7
- data/spec/sql_inspections/arel/inet_spec.rb +7 -7
- data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
- data/spec/sql_inspections/either_sql_spec.rb +11 -11
- data/spec/sql_inspections/json_sql_spec.rb +44 -8
- data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
- data/spec/sql_inspections/window_sql_spec.rb +98 -0
- data/spec/sql_inspections/with_cte_sql_spec.rb +52 -23
- data/spec/support/models.rb +24 -4
- metadata +31 -20
- data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
- data/lib/active_record_extended/utilities.rb +0 -141
@@ -28,7 +28,7 @@ module ActiveRecordExtended
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def hash_map_queries(queries)
|
31
|
-
if queries.
|
31
|
+
if queries.size == 1 && queries.first.is_a?(Hash)
|
32
32
|
queries.first.each_pair.map { |attr, predicate| Hash[attr, predicate] }
|
33
33
|
else
|
34
34
|
queries
|
@@ -38,9 +38,10 @@ module ActiveRecordExtended
|
|
38
38
|
def build_query(queries)
|
39
39
|
query_map = construct_query_mappings(queries)
|
40
40
|
query = yield(query_map[:arel_query], query_map[:binds])
|
41
|
-
query
|
42
|
-
|
43
|
-
|
41
|
+
query
|
42
|
+
.joins(query_map[:joins].to_a)
|
43
|
+
.includes(query_map[:includes].to_a)
|
44
|
+
.references(query_map[:references].to_a)
|
44
45
|
end
|
45
46
|
|
46
47
|
def construct_query_mappings(queries) # rubocop:disable Metrics/AbcSize
|
@@ -60,13 +61,14 @@ module ActiveRecordExtended
|
|
60
61
|
# In Rails 5.2 the arel table maintains attribute binds
|
61
62
|
def bind_attributes(query)
|
62
63
|
return [] unless query.respond_to?(:bound_attributes)
|
64
|
+
|
63
65
|
query.bound_attributes.map(&:value)
|
64
66
|
end
|
65
67
|
|
66
68
|
# Rails 5.1 fix
|
67
69
|
def unprepared_query(query)
|
68
|
-
query.gsub(/((?<!\\)'.*?(?<!\\)'|(?<!\\)".*?(?<!\\)")|(
|
69
|
-
Regexp.last_match(2)&.gsub(
|
70
|
+
query.gsub(/((?<!\\)'.*?(?<!\\)'|(?<!\\)".*?(?<!\\)")|(=\ \$\d+)/) do |match|
|
71
|
+
Regexp.last_match(2)&.gsub(/=\ \$\d+/, "= ?") || match
|
70
72
|
end
|
71
73
|
end
|
72
74
|
|
@@ -77,9 +79,9 @@ module ActiveRecordExtended
|
|
77
79
|
def generate_where_clause(query)
|
78
80
|
case query
|
79
81
|
when String, Hash
|
80
|
-
@scope.where(query)
|
82
|
+
@scope.unscoped.where(query)
|
81
83
|
when Array
|
82
|
-
@scope.where(*query)
|
84
|
+
@scope.unscoped.where(*query)
|
83
85
|
else
|
84
86
|
query
|
85
87
|
end
|
@@ -57,7 +57,7 @@ module ActiveRecordExtended
|
|
57
57
|
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" >> '127.0.0.255/32'"
|
58
58
|
#
|
59
59
|
def inet_contains(opts, *rest)
|
60
|
-
substitute_comparisons(opts, rest, Arel::Nodes::Contains, "inet_contains")
|
60
|
+
substitute_comparisons(opts, rest, Arel::Nodes::Inet::Contains, "inet_contains")
|
61
61
|
end
|
62
62
|
|
63
63
|
# This method is a combination of `inet_contains` and `inet_contained_within`
|
@@ -74,8 +74,12 @@ module ActiveRecordExtended
|
|
74
74
|
# #=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"ip\" && '127.0.0.255/32'"
|
75
75
|
#
|
76
76
|
def inet_contains_or_is_contained_within(opts, *rest)
|
77
|
-
substitute_comparisons(
|
78
|
-
|
77
|
+
substitute_comparisons(
|
78
|
+
opts,
|
79
|
+
rest,
|
80
|
+
Arel::Nodes::Inet::ContainsOrContainedWithin,
|
81
|
+
"inet_contains_or_is_contained_within"
|
82
|
+
)
|
79
83
|
end
|
80
84
|
end
|
81
85
|
end
|
@@ -8,46 +8,50 @@ module ActiveRecordExtended
|
|
8
8
|
:json_build_object,
|
9
9
|
:jsonb_build_object,
|
10
10
|
:json_build_literal,
|
11
|
-
:jsonb_build_literal
|
11
|
+
:jsonb_build_literal
|
12
12
|
].freeze
|
13
13
|
|
14
14
|
class JsonChain
|
15
|
-
include ::ActiveRecordExtended::Utilities
|
16
|
-
|
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
|
17
21
|
|
18
22
|
def initialize(scope)
|
19
23
|
@scope = scope
|
20
24
|
end
|
21
25
|
|
22
26
|
def row_to_json!(**args, &block)
|
23
|
-
options = json_object_options(args
|
27
|
+
options = json_object_options(args, except: [:values, :value])
|
24
28
|
build_row_to_json(**options, &block)
|
25
29
|
end
|
26
30
|
|
27
31
|
def json_build_object!(*args)
|
28
|
-
options = json_object_options(args
|
32
|
+
options = json_object_options(args, except: [:values, :cast_with, :order_by])
|
29
33
|
build_json_object(Arel::Nodes::JsonBuildObject, **options)
|
30
34
|
end
|
31
35
|
|
32
36
|
def jsonb_build_object!(*args)
|
33
|
-
options = json_object_options(args
|
37
|
+
options = json_object_options(args, except: [:values, :cast_with, :order_by])
|
34
38
|
build_json_object(Arel::Nodes::JsonbBuildObject, **options)
|
35
39
|
end
|
36
40
|
|
37
41
|
def json_build_literal!(*args)
|
38
|
-
options = json_object_options(args
|
42
|
+
options = json_object_options(args, only: [:values, :col_alias])
|
39
43
|
build_json_literal(Arel::Nodes::JsonBuildObject, **options)
|
40
44
|
end
|
41
45
|
|
42
46
|
def jsonb_build_literal!(*args)
|
43
|
-
options = json_object_options(args
|
47
|
+
options = json_object_options(args, only: [:values, :col_alias])
|
44
48
|
build_json_literal(Arel::Nodes::JsonbBuildObject, **options)
|
45
49
|
end
|
46
50
|
|
47
51
|
private
|
48
52
|
|
49
53
|
def build_json_literal(arel_klass, values:, col_alias: DEFAULT_ALIAS)
|
50
|
-
json_values = flatten_to_sql(values.to_a
|
54
|
+
json_values = flatten_to_sql(values.to_a) { |value| literal_key(value) }
|
51
55
|
col_alias = double_quote(col_alias)
|
52
56
|
json_build_obj = arel_klass.new(json_values)
|
53
57
|
@scope.select(nested_alias_escape(json_build_obj, col_alias))
|
@@ -60,42 +64,82 @@ module ActiveRecordExtended
|
|
60
64
|
col_value = to_arel_sql(value.presence || tbl_alias)
|
61
65
|
json_build_object = arel_klass.new(to_sql_array(col_key, col_value))
|
62
66
|
|
63
|
-
|
64
|
-
unless col_value.index(/".+"/)
|
67
|
+
unless /".+"/.match?(col_value)
|
65
68
|
warn("`#{col_value}`: the `value` argument should contain a double quoted key reference for safety")
|
66
69
|
end
|
67
70
|
|
68
71
|
@scope.select(nested_alias_escape(json_build_object, col_alias)).from(nested_alias_escape(from, tbl_alias))
|
69
72
|
end
|
70
73
|
|
71
|
-
def build_row_to_json(from:,
|
72
|
-
|
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
|
+
|
73
79
|
dummy_table = from_clause_constructor(from, key).select(row_to_json)
|
74
|
-
dummy_table =
|
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
|
75
86
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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)
|
80
96
|
else
|
81
|
-
|
97
|
+
nested_alias_escape(dummy_table, col_alias)
|
82
98
|
end
|
83
99
|
end
|
84
100
|
|
85
|
-
def json_object_options(
|
86
|
-
|
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|
|
87
112
|
next if arg.nil?
|
88
113
|
|
89
114
|
if arg.is_a?(Hash)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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)
|
95
126
|
end
|
127
|
+
end
|
128
|
+
|
129
|
+
options.tap(&:compact!)
|
130
|
+
end
|
131
|
+
|
132
|
+
def casting_options(cast_with)
|
133
|
+
return {} if cast_with.blank?
|
96
134
|
|
97
|
-
|
98
|
-
|
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
|
99
143
|
end
|
100
144
|
end
|
101
145
|
|
@@ -110,25 +154,102 @@ module ActiveRecordExtended
|
|
110
154
|
# - key: [Symbol or String] (default=[random letter]) What the row clause will be set as.
|
111
155
|
# - This is useful if you would like to add additional mid-level clauses (see mid-level scope example)
|
112
156
|
#
|
113
|
-
# -
|
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)
|
114
162
|
#
|
115
|
-
#
|
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:
|
116
170
|
# subquery = Group.select(:name, :category_id).where("user_id = users.id")
|
117
|
-
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups,
|
171
|
+
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
|
118
172
|
# #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
|
119
173
|
#
|
120
174
|
# - Adding mid-level scopes:
|
121
175
|
#
|
122
176
|
# subquery = Group.select(:name, :category_id)
|
123
|
-
# User.select_row_to_json(subquery, key: :group,
|
177
|
+
# User.select_row_to_json(subquery, key: :group, cast_with: :array) do |scope|
|
124
178
|
# scope.where(group: { name: "Nerd Core" })
|
125
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
|
+
# ```
|
126
247
|
#
|
127
|
-
|
128
248
|
def select_row_to_json(from = nil, **options, &block)
|
129
249
|
from.is_a?(Hash) ? options.merge!(from) : options.reverse_merge!(from: from)
|
130
250
|
options.compact!
|
131
|
-
raise ArgumentError
|
251
|
+
raise ArgumentError.new("Required to provide a non-nilled from clause") unless options.key?(:from)
|
252
|
+
|
132
253
|
JsonChain.new(spawn).row_to_json!(**options, &block)
|
133
254
|
end
|
134
255
|
|
@@ -149,7 +270,7 @@ module ActiveRecordExtended
|
|
149
270
|
# - Generic example:
|
150
271
|
#
|
151
272
|
# subquery = Group.select(:name, :category_id).where("user_id = users.id")
|
152
|
-
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups,
|
273
|
+
# User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_with: :array)
|
153
274
|
# #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
|
154
275
|
#
|
155
276
|
# - Setting a custom value:
|
@@ -163,21 +284,6 @@ module ActiveRecordExtended
|
|
163
284
|
# .take
|
164
285
|
# .results["gang_members"] #=> "BANG!"
|
165
286
|
#
|
166
|
-
#
|
167
|
-
# - Adding mid-level scopes
|
168
|
-
#
|
169
|
-
# subquery = Group.select(:name, :category_id)
|
170
|
-
# User.select_row_to_json(subquery, key: :group, cast_as_array: true) do |scope|
|
171
|
-
# scope.where(group: { name: "Nerd Core" })
|
172
|
-
# end #=> ```sql
|
173
|
-
# SELECT ARRAY(
|
174
|
-
# SELECT ROW_TO_JSON("group")
|
175
|
-
# FROM(SELECT name, category_id FROM groups) AS group
|
176
|
-
# WHERE group.name = 'Nerd Core'
|
177
|
-
# )
|
178
|
-
# ```
|
179
|
-
#
|
180
|
-
|
181
287
|
def json_build_object(key, from, **options)
|
182
288
|
options[:key] = key
|
183
289
|
options[:from] = from
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Select
|
6
|
+
class SelectHelper
|
7
|
+
include ::ActiveRecordExtended::Utilities::Support
|
8
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
9
|
+
|
10
|
+
AGGREGATE_ONE_LINERS = /^(exists|sum|max|min|avg|count|jsonb?_agg|(bit|bool)_(and|or)|xmlagg|array_agg)$/.freeze
|
11
|
+
|
12
|
+
def initialize(scope)
|
13
|
+
@scope = scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_foster_select(*args)
|
17
|
+
flatten_safely(args).each do |select_arg|
|
18
|
+
case select_arg
|
19
|
+
when String, Symbol
|
20
|
+
select!(select_arg)
|
21
|
+
when Hash
|
22
|
+
select_arg.each_pair do |alias_name, options_or_column|
|
23
|
+
case options_or_column
|
24
|
+
when Array
|
25
|
+
process_array!(options_or_column, alias_name)
|
26
|
+
when Hash
|
27
|
+
process_hash!(options_or_column, alias_name)
|
28
|
+
else
|
29
|
+
select!(options_or_column, alias_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Assumes that the first element in the array is the source/target column.
|
39
|
+
# Example
|
40
|
+
# process_array_options!([:col_name], :my_alias_name)
|
41
|
+
# #=> SELECT ([:col_name:]) AS "my_alias_name", ...
|
42
|
+
def process_array!(array_of_options, alias_name)
|
43
|
+
options = array_of_options.detect { |opts| opts.is_a?(Hash) }
|
44
|
+
query = { __select_statement: array_of_options.first }
|
45
|
+
query.merge!(options) unless options.nil?
|
46
|
+
process_hash!(query, alias_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Processes options that come in as Hash elements
|
50
|
+
# Examples:
|
51
|
+
# process_hash_options!({ memberships: :price, cast_with: :agg_array_distinct }, :past_purchases)
|
52
|
+
# #=> SELECT (ARRAY_AGG(DISTINCT members.price)) AS past_purchases, ...
|
53
|
+
def process_hash!(hash_of_options, alias_name)
|
54
|
+
enforced_options = {
|
55
|
+
cast_with: hash_of_options[:cast_with],
|
56
|
+
order_by: hash_of_options[:order_by],
|
57
|
+
distinct: !(!hash_of_options[:distinct])
|
58
|
+
}
|
59
|
+
query_statement = hash_to_dot_notation(hash_of_options[:__select_statement] || hash_of_options.first)
|
60
|
+
select!(query_statement, alias_name, **enforced_options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Turn a hash chain into a query statement:
|
64
|
+
# Example: hash_to_dot_notation(table_name: :col_name) #=> "table_name.col_name"
|
65
|
+
def hash_to_dot_notation(column)
|
66
|
+
case column
|
67
|
+
when Hash, Array
|
68
|
+
column.to_a.flat_map { |col| hash_to_dot_notation(col) }.join(".")
|
69
|
+
when String, Symbol
|
70
|
+
/^([[:alpha:]]+)$/.match?(column.to_s) ? double_quote(column) : column
|
71
|
+
else
|
72
|
+
column
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add's select statement values to the current relation, select statement lists
|
77
|
+
def select!(query, alias_name = nil, **options)
|
78
|
+
pipe_cte_with!(query)
|
79
|
+
@scope._select!(to_casted_query(query, alias_name, **options))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Wraps the query with the requested query method
|
83
|
+
# Example:
|
84
|
+
# to_casted_query("memberships.cost", :total_revenue, :sum)
|
85
|
+
# #=> SELECT (SUM(memberships.cost)) AS total_revenue
|
86
|
+
def to_casted_query(query, alias_name, **options)
|
87
|
+
cast_with = options[:cast_with].to_s.downcase
|
88
|
+
order_expr = order_by_expression(options[:order_by])
|
89
|
+
distinct = cast_with.chomp!("_distinct") || options[:distinct] # account for [:agg_name:]_distinct
|
90
|
+
|
91
|
+
case cast_with
|
92
|
+
when "array", "true"
|
93
|
+
wrap_with_array(query, alias_name)
|
94
|
+
when AGGREGATE_ONE_LINERS
|
95
|
+
expr = to_sql_array(query) { |value| group_when_needed(value) }
|
96
|
+
casted_query = ::Arel::Nodes::AggregateFunctionName.new(cast_with, expr, distinct).order_by(order_expr)
|
97
|
+
nested_alias_escape(casted_query, alias_name)
|
98
|
+
else
|
99
|
+
alias_name.presence ? nested_alias_escape(query, alias_name) : query
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def foster_select(*args)
|
105
|
+
raise ArgumentError.new("Call `.forster_select' with at least one field") if args.empty?
|
106
|
+
|
107
|
+
spawn._foster_select!(*args)
|
108
|
+
end
|
109
|
+
|
110
|
+
def _foster_select!(*args)
|
111
|
+
SelectHelper.new(self).build_foster_select(*args)
|
112
|
+
self
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Select)
|