active_record_extended 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)