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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -15
  3. data/lib/active_record_extended.rb +2 -1
  4. data/lib/active_record_extended/active_record.rb +2 -9
  5. data/lib/active_record_extended/active_record/relation_patch.rb +21 -4
  6. data/lib/active_record_extended/arel.rb +2 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +32 -41
  9. data/lib/active_record_extended/arel/predications.rb +4 -1
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
  12. data/lib/active_record_extended/query_methods/any_of.rb +10 -8
  13. data/lib/active_record_extended/query_methods/either.rb +1 -1
  14. data/lib/active_record_extended/query_methods/inet.rb +7 -3
  15. data/lib/active_record_extended/query_methods/json.rb +156 -50
  16. data/lib/active_record_extended/query_methods/select.rb +118 -0
  17. data/lib/active_record_extended/query_methods/unionize.rb +14 -43
  18. data/lib/active_record_extended/query_methods/where_chain.rb +14 -6
  19. data/lib/active_record_extended/query_methods/window.rb +93 -0
  20. data/lib/active_record_extended/query_methods/with_cte.rb +102 -35
  21. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  22. data/lib/active_record_extended/utilities/support.rb +178 -0
  23. data/lib/active_record_extended/version.rb +1 -1
  24. data/spec/query_methods/any_of_spec.rb +40 -40
  25. data/spec/query_methods/array_query_spec.rb +14 -14
  26. data/spec/query_methods/either_spec.rb +14 -14
  27. data/spec/query_methods/hash_query_spec.rb +11 -11
  28. data/spec/query_methods/inet_query_spec.rb +33 -31
  29. data/spec/query_methods/json_spec.rb +42 -27
  30. data/spec/query_methods/select_spec.rb +115 -0
  31. data/spec/query_methods/unionize_spec.rb +56 -56
  32. data/spec/query_methods/window_spec.rb +51 -0
  33. data/spec/query_methods/with_cte_spec.rb +22 -12
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/sql_inspections/any_of_sql_spec.rb +12 -12
  36. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  37. data/spec/sql_inspections/arel/array_spec.rb +7 -7
  38. data/spec/sql_inspections/arel/inet_spec.rb +7 -7
  39. data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
  40. data/spec/sql_inspections/either_sql_spec.rb +11 -11
  41. data/spec/sql_inspections/json_sql_spec.rb +44 -8
  42. data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
  43. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  44. data/spec/sql_inspections/with_cte_sql_spec.rb +52 -23
  45. data/spec/support/models.rb +24 -4
  46. metadata +31 -20
  47. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
  48. 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.count == 1 && queries.first.is_a?(Hash)
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.joins(query_map[:joins].to_a)
42
- .includes(query_map[:includes].to_a)
43
- .references(query_map[:references].to_a)
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(/((?<!\\)'.*?(?<!\\)'|(?<!\\)".*?(?<!\\)")|(\=\ \$\d+)/) do |match|
69
- Regexp.last_match(2)&.gsub(/\=\ \$\d+/, "= ?") || match
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
@@ -30,7 +30,7 @@ module ActiveRecordExtended
30
30
  end
31
31
 
32
32
  def sort_order_sql(dir)
33
- %w[asc desc].include?(dir.to_s) ? dir.to_s : "asc"
33
+ ["asc", "desc"].include?(dir.to_s) ? dir.to_s : "asc"
34
34
  end
35
35
 
36
36
  def xor_field_options(options)
@@ -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(opts, rest, Arel::Nodes::Inet::ContainsOrContainedWithin,
78
- "inet_contains_or_is_contained_within")
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
- DEFAULT_ALIAS = '"results"'
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).except(:values, :value)
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).except!(:values)
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).except!(:values)
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).slice(:values, :col_alias)
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).slice(:values, :col_alias)
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, &method(:literal_key))
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
- # TODO: Change this to #match?(..) when we drop Rails 5.0 or Ruby 2.4 support
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:, key: key_generator, col_alias: nil, cast_to_array: false)
72
- row_to_json = Arel::Nodes::RowToJson.new(double_quote(key))
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 = yield dummy_table if block_given?
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
- if col_alias.blank?
77
- dummy_table
78
- elsif cast_to_array
79
- @scope.select(wrap_with_array(dummy_table, col_alias))
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
- @scope.select(nested_alias_escape(dummy_table, col_alias))
97
+ nested_alias_escape(dummy_table, col_alias)
82
98
  end
83
99
  end
84
100
 
85
- def json_object_options(*args) # rubocop:disable Metrics/AbcSize
86
- flatten_safely(args).each_with_object(values: []) do |arg, options|
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
- options[:key] ||= arg.delete(:key)
91
- options[:value] ||= arg.delete(:value).presence
92
- options[:col_alias] ||= arg.delete(:as)
93
- options[:cast_to_array] ||= arg.delete(:cast_as_array)
94
- options[:from] ||= arg.delete(:from).tap(&method(:pipe_cte_with!))
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
- options[:values] << (arg.respond_to?(:to_a) ? arg.to_a : arg)
98
- end.compact
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
- # - cast_as_array [boolean] (default=false): Determines if the query should be nested inside an Array() function
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
- # Example:
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, cast_as_array: true)
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, cast_as_array: true) do |scope|
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, "Required to provide a non-nilled from clause" unless options.key?(:from)
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, cast_as_array: true)
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)