active_record_extended 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -18
  3. data/lib/active_record_extended.rb +2 -1
  4. data/lib/active_record_extended/active_record.rb +2 -0
  5. data/lib/active_record_extended/active_record/relation_patch.rb +1 -1
  6. data/lib/active_record_extended/arel.rb +1 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +31 -41
  9. data/lib/active_record_extended/arel/predications.rb +4 -1
  10. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
  11. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +4 -4
  12. data/lib/active_record_extended/patch/5_0/regex_match.rb +10 -0
  13. data/lib/active_record_extended/query_methods/any_of.rb +5 -4
  14. data/lib/active_record_extended/query_methods/inet.rb +1 -1
  15. data/lib/active_record_extended/query_methods/json.rb +152 -43
  16. data/lib/active_record_extended/query_methods/select.rb +117 -0
  17. data/lib/active_record_extended/query_methods/unionize.rb +4 -39
  18. data/lib/active_record_extended/query_methods/with_cte.rb +3 -3
  19. data/lib/active_record_extended/utilities/order_by.rb +96 -0
  20. data/lib/active_record_extended/utilities/support.rb +175 -0
  21. data/lib/active_record_extended/version.rb +1 -1
  22. data/spec/query_methods/any_of_spec.rb +40 -40
  23. data/spec/query_methods/array_query_spec.rb +14 -14
  24. data/spec/query_methods/either_spec.rb +14 -14
  25. data/spec/query_methods/hash_query_spec.rb +11 -11
  26. data/spec/query_methods/inet_query_spec.rb +33 -31
  27. data/spec/query_methods/json_spec.rb +40 -25
  28. data/spec/query_methods/select_spec.rb +115 -0
  29. data/spec/query_methods/unionize_spec.rb +54 -54
  30. data/spec/query_methods/with_cte_spec.rb +12 -12
  31. data/spec/sql_inspections/any_of_sql_spec.rb +11 -11
  32. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  33. data/spec/sql_inspections/arel/array_spec.rb +7 -7
  34. data/spec/sql_inspections/arel/inet_spec.rb +7 -7
  35. data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
  36. data/spec/sql_inspections/either_sql_spec.rb +11 -11
  37. data/spec/sql_inspections/json_sql_spec.rb +38 -8
  38. data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
  39. data/spec/sql_inspections/with_cte_sql_spec.rb +22 -22
  40. data/spec/support/models.rb +18 -4
  41. metadata +11 -3
  42. data/lib/active_record_extended/utilities.rb +0 -141
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9600653726c77cad5b6e0a90c1f5a55213c95804a3b094c1595aa085127f2605
4
- data.tar.gz: 0a7204abdd56e9d92c6b304d42d79b7270feb966aed55cc122e2c2ad68d91dab
3
+ metadata.gz: b524c24c37e0b6f7449d70e5030b893119b98dec6646b905c3a307c9a671c7f1
4
+ data.tar.gz: 18cb235fba334ebfaa752b92ad024117f686176b0e9107af6333e317a333c70f
5
5
  SHA512:
6
- metadata.gz: '008286c5485c0e14d8fd113ac12c78f2079f572ad5d99bf4eee9c8c3fa16c58948c496aa79436718098a120d3481aa832a2d9509f89beb7ee7733e2d96754ae3'
7
- data.tar.gz: e239092f0511f1c75f2eca0a0aed42f193d14a6f4c490dc6549c87c19c231b671d5a3316be6dcaea0641d6c633f33f93ae8618a43f7b5314d1d97ad2abda7fd3
6
+ metadata.gz: 78539255c169c26a1d6bab3968a2eed038d8391d532f97d55dabf7a7c071cddf4eee1424586021cd7b790db4abf042cf0617ac69ede670caeb9cde55a23125a4
7
+ data.tar.gz: 798ea355e5ebe213735db1552e7328d05c855c45800c941d4c5400a812b6d71f169ff52a48c0b9e6bf678d2dcf83ac1c7308a172b120caf9f83a240b0025e599
data/README.md CHANGED
@@ -46,7 +46,6 @@ The only problem is that this has created a wild west of environments of sorts.
46
46
 
47
47
  Active Record Extended is essentially providing users with the other half of Postgreses querying abilities. Due to Rails/ActiveRecord/Arel being designed to be DB agnostic, there are a lot of left out features; Either by choice or the simple lack of supporting API's for other databases. However some features are not exactly PG explicit. Some are just helper methods to express an idea much more easily.
48
48
 
49
-
50
49
  ## Compatibility
51
50
 
52
51
  This package is designed align and work with any officially supported Ruby and Rails versions.
@@ -56,6 +55,18 @@ This package is designed align and work with any officially supported Ruby and R
56
55
  - Latest Rails supported: 6.0.x
57
56
  - Postgres: 9.6-current(11) (probably works with most older versions to a certain point)
58
57
 
58
+ ## Installation
59
+
60
+ Add this line to your application's Gemfile:
61
+
62
+ ```ruby
63
+ gem 'active_record_extended'
64
+ ```
65
+
66
+ And then execute:
67
+
68
+ $ bundle
69
+
59
70
  ## Usage
60
71
 
61
72
  ### Predicate Query Methods
@@ -776,23 +787,6 @@ SELECT "people".*
776
787
  ) ) ORDER BY personal_id DESC, id DESC) people
777
788
  ```
778
789
 
779
-
780
- ## Installation
781
-
782
- Add this line to your application's Gemfile:
783
-
784
- ```ruby
785
- gem 'active_record_extended'
786
- ```
787
-
788
- And then execute:
789
-
790
- $ bundle
791
-
792
- Or install it yourself as:
793
-
794
- $ gem install active_record_extended
795
-
796
790
  ## License
797
791
 
798
792
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/version"
4
- require "active_record_extended/utilities"
4
+ require "active_record_extended/utilities/support"
5
+ require "active_record_extended/utilities/order_by"
5
6
  require "active_record_extended/active_record"
6
7
  require "active_record_extended/arel"
7
8
 
@@ -21,9 +21,11 @@ require "active_record_extended/query_methods/any_of"
21
21
  require "active_record_extended/query_methods/either"
22
22
  require "active_record_extended/query_methods/inet"
23
23
  require "active_record_extended/query_methods/json"
24
+ require "active_record_extended/query_methods/select"
24
25
 
25
26
  if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR <= 1
26
27
  if ActiveRecord::VERSION::MINOR.zero?
28
+ require "active_record_extended/patch/5_0/regex_match"
27
29
  require "active_record_extended/patch/5_0/predicate_builder_decorator"
28
30
  end
29
31
  require "active_record_extended/patch/5_1/where_clause"
@@ -6,7 +6,7 @@ require "active_record_extended/query_methods/json"
6
6
  module ActiveRecordExtended
7
7
  module RelationPatch
8
8
  module QueryDelegation
9
- delegate :with, to: :all
9
+ delegate :with, :foster_select, to: :all
10
10
  delegate(*::ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS, to: :all)
11
11
  delegate(*::ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS, to: :all)
12
12
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/arel/nodes"
4
+ require "active_record_extended/arel/aggregate_function_name"
4
5
  require "active_record_extended/arel/predications"
5
6
  require "active_record_extended/arel/visitors/postgresql_decorator"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel
4
+ module Nodes
5
+ class AggregateFunctionName < ::Arel::Nodes::Node
6
+ include Arel::Predications
7
+ include Arel::WindowPredications
8
+ attr_accessor :name, :expressions, :distinct, :alias, :orderings
9
+
10
+ def initialize(name, expr, distinct = false)
11
+ super()
12
+ @name = name.to_s.upcase
13
+ @expressions = expr
14
+ @distinct = distinct
15
+ end
16
+
17
+ def order_by(expr)
18
+ @orderings = expr
19
+ self
20
+ end
21
+
22
+ def as(aliaz)
23
+ self.alias = SqlLiteral.new(aliaz)
24
+ self
25
+ end
26
+
27
+ def hash
28
+ [@name, @expressions, @distinct, @alias, @orderings].hash
29
+ end
30
+
31
+ def eql?(other)
32
+ self.class == other.class &&
33
+ expressions == other.expressions &&
34
+ orderings == other.orderings &&
35
+ distinct == other.distinct
36
+ end
37
+ alias == eql?
38
+ end
39
+ end
40
+ end
@@ -1,56 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "arel/nodes/binary"
4
+ require "arel/nodes/function"
4
5
 
5
6
  module Arel
6
7
  module Nodes
7
- class Overlap < Arel::Nodes::Binary
8
- end
9
-
10
- class Contains < Arel::Nodes::Binary
11
- end
12
-
13
- class ContainsHStore < Arel::Nodes::Binary
14
- end
15
-
16
- class ContainsArray < Arel::Nodes::Binary
17
- end
18
-
19
- class ContainedInArray < Arel::Nodes::Binary
20
- end
21
-
22
- class RowToJson < Arel::Nodes::Function
23
- def initialize(*args)
24
- super
25
- unless @expressions.is_a?(Array)
26
- @expressions = Arel.sql(@expressions) unless @expressions.is_a?(Arel::Nodes::SqlLiteral)
27
- @expressions = [@expressions]
8
+ %w[
9
+ Overlap
10
+ Contains
11
+ ContainsHStore
12
+ ContainsArray
13
+ ContainedInArray
14
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
15
+
16
+ %w[
17
+ RowToJson
18
+ JsonBuildObject
19
+ JsonbBuildObject
20
+ ToJson
21
+ ToJsonb
22
+ Array
23
+ ArrayAgg
24
+ ].each do |function_node_name|
25
+ func_klass = Class.new(::Arel::Nodes::Function) do
26
+ def initialize(*args)
27
+ super
28
+ return if @expressions.is_a?(::Array)
29
+ @expressions = @expressions.is_a?(::Arel::Node) ? [@expressions] : [::Arel.sql(@expressions)]
28
30
  end
29
31
  end
30
- end
31
32
 
32
- class JsonBuildObject < Arel::Nodes::Function
33
- def initialize(*args)
34
- super
35
- @expressions = Array(@expressions)
36
- end
37
- end
38
-
39
- class JsonbBuildObject < JsonBuildObject
33
+ const_set(function_node_name, func_klass)
40
34
  end
41
35
 
42
36
  module Inet
43
- class ContainsEquals < Arel::Nodes::Binary
44
- end
45
-
46
- class ContainedWithin < Arel::Nodes::Binary
47
- end
48
-
49
- class ContainedWithinEquals < Arel::Nodes::Binary
50
- end
51
-
52
- class ContainsOrContainedWithin < Arel::Nodes::Binary
53
- end
37
+ %w[
38
+ Contains
39
+ ContainsEquals
40
+ ContainedWithin
41
+ ContainedWithinEquals
42
+ ContainsOrContainedWithin
43
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
54
44
  end
55
45
  end
56
46
  end
@@ -21,12 +21,15 @@ module Arel
21
21
  def contains(other)
22
22
  Nodes::Contains.new self, Nodes.build_quoted(other, self)
23
23
  end
24
- alias inet_contains contains
25
24
 
26
25
  def contained_in_array(other)
27
26
  Nodes::ContainedInArray.new self, Nodes.build_quoted(other, self)
28
27
  end
29
28
 
29
+ def inet_contains(other)
30
+ Nodes::Inet::Contains.new self, Nodes.build_quoted(other, self)
31
+ end
32
+
30
33
  def inet_contains_or_is_contained_within(other)
31
34
  Nodes::Inet::ContainsOrContainedWithin.new self, Nodes.build_quoted(other, self)
32
35
  end
@@ -23,7 +23,7 @@ module ActiveRecordExtended
23
23
  elsif left_column.try(:array)
24
24
  visit_Arel_Nodes_ContainsArray(object, collector)
25
25
  else
26
- infix_value object, collector, " >> "
26
+ visit_Arel_Nodes_Inet_Contains(object, collector)
27
27
  end
28
28
  end
29
29
 
@@ -59,6 +59,45 @@ module ActiveRecordExtended
59
59
  aggregate "JSONB_BUILD_OBJECT", object, collector
60
60
  end
61
61
 
62
+ def visit_Arel_Nodes_ToJson(object, collector)
63
+ aggregate "TO_JSON", object, collector
64
+ end
65
+
66
+ def visit_Arel_Nodes_ToJsonb(object, collector)
67
+ aggregate "TO_JSONB", object, collector
68
+ end
69
+
70
+ def visit_Arel_Nodes_Array(object, collector)
71
+ aggregate "ARRAY", object, collector
72
+ end
73
+
74
+ def visit_Arel_Nodes_ArrayAgg(object, collector)
75
+ aggregate "ARRAY_AGG", object, collector
76
+ end
77
+
78
+ def visit_Arel_Nodes_AggregateFunctionName(object, collector)
79
+ collector << "#{object.name}("
80
+ collector << "DISTINCT " if object.distinct
81
+ collector = inject_join(object.expressions, collector, ", ")
82
+
83
+ if object.orderings
84
+ collector << " ORDER BY "
85
+ collector = inject_join(object.orderings, collector, ", ")
86
+ end
87
+ collector << ")"
88
+
89
+ if object.alias
90
+ collector << " AS "
91
+ visit object.alias, collector
92
+ else
93
+ collector
94
+ end
95
+ end
96
+
97
+ def visit_Arel_Nodes_Inet_Contains(object, collector)
98
+ infix_value object, collector, " >> "
99
+ end
100
+
62
101
  def visit_Arel_Nodes_Inet_ContainedWithinEquals(object, collector)
63
102
  infix_value object, collector, " <<= "
64
103
  end
@@ -6,20 +6,20 @@
6
6
  #
7
7
  # Without joins
8
8
  # Before:
9
- # Person.where.contains(data: { nickname: "george" })
9
+ # User.where.contains(data: { nickname: "george" })
10
10
  # #=> "SELECT \"people\".* FROM \"people\" WHERE (\"data\".\"nickname\" @> 'george')"
11
11
  #
12
12
  # After:
13
- # Person.where.contains(data: { nickname: "george" })
13
+ # User.where.contains(data: { nickname: "george" })
14
14
  # #=> "SELECT \"people\".* FROM \"people\" WHERE (\"people\".\"data\" @> '\"nickname\"=>\"george\"')"
15
15
  #
16
16
  # With Joins
17
17
  # Before:
18
- # Tag.joins(:person).where.contains(people: { data: { nickname: "george" } })
18
+ # Tag.joins(:user).where.contains(people: { data: { nickname: "george" } })
19
19
  # #=> NoMethodError: undefined method `type' for nil:NilClass
20
20
  #
21
21
  # After:
22
- # Tag.joins(:person).where.contains(people: { data: { nickname: "george" } })
22
+ # Tag.joins(:user).where.contains(people: { data: { nickname: "george" } })
23
23
  # #=> "SELECT \"tags\".* FROM \"tags\" INNER JOIN \"people\" ON \"people\".\"id\" = \"tags\".\"person_id\"
24
24
  # WHERE (\"people\".\"data\" @> '\"nickname\"=>\"george\"')"
25
25
  #
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Regexp
4
+ # Stripped from ActiveSupport v5.1
5
+ unless //.respond_to?(:match?)
6
+ def match?(string, pos = 0)
7
+ !(!match(string, pos))
8
+ end
9
+ end
10
+ end
@@ -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
@@ -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`
@@ -12,35 +12,39 @@ module ActiveRecordExtended
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
 
@@ -60,42 +64,83 @@ 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_given?
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
+ # TODO: [V2 release] Drop support for option :cast_as_array in favor of a more versatile :cast_with option
102
+ def json_object_options(args, except: [], only: []) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
103
+ options = {}
104
+ lean_opts = lambda do |key, &block|
105
+ if only.present?
106
+ options[key] ||= block.call if only.include?(key)
107
+ elsif !except.include?(key)
108
+ options[key] ||= block.call
109
+ end
110
+ end
111
+
112
+ flatten_safely(args) do |arg|
87
113
  next if arg.nil?
88
114
 
89
115
  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!))
116
+ lean_opts.call(:key) { arg.delete(:key) || key_generator }
117
+ lean_opts.call(:value) { arg.delete(:value).presence }
118
+ lean_opts.call(:col_alias) { arg.delete(:as) }
119
+ lean_opts.call(:order_by) { order_by_expression(arg.delete(:order_by)) }
120
+ lean_opts.call(:from) { arg.delete(:from).tap(&method(:pipe_cte_with!)) }
121
+ lean_opts.call(:cast_with) { casting_options(arg.delete(:cast_with) || arg.delete(:cast_as_array)) }
95
122
  end
96
123
 
97
- options[:values] << (arg.respond_to?(:to_a) ? arg.to_a : arg)
98
- end.compact
124
+ unless except.include?(:values)
125
+ options[:values] ||= []
126
+ options[:values] << (arg.respond_to?(:to_a) ? arg.to_a : arg)
127
+ end
128
+ end
129
+
130
+ options.tap(&:compact!)
131
+ end
132
+
133
+ def casting_options(cast_with)
134
+ return {} if cast_with.blank?
135
+
136
+ skip_convert = [Symbol, TrueClass, FalseClass]
137
+ Array(cast_with).compact.each_with_object({}) do |arg, options|
138
+ arg = arg.to_sym unless skip_convert.include?(arg.class)
139
+ options[:to_jsonb] |= TO_JSONB_OPTIONS.include?(arg)
140
+ options[:array] |= ARRAY_OPTIONS.include?(arg)
141
+ options[:array_agg] |= arg == :array_agg
142
+ options[:distinct] |= arg == :distinct
143
+ end
99
144
  end
100
145
  end
101
146
 
@@ -111,8 +156,21 @@ module ActiveRecordExtended
111
156
  # - This is useful if you would like to add additional mid-level clauses (see mid-level scope example)
112
157
  #
113
158
  # - cast_as_array [boolean] (default=false): Determines if the query should be nested inside an Array() function
159
+ # * Will be deprecated in V2.0 in favor of `cast_with` argument
114
160
  #
115
- # Example:
161
+ # - cast_with [Symbol or Array of symbols]: Actions to transform your query
162
+ # * :to_jsonb
163
+ # * :array
164
+ # * :array_agg (including just :array with this option will favor :array_agg)
165
+ # * :distinct (auto applies :array_agg & :to_jsonb)
166
+ #
167
+ # - order_by [Symbol or hash]: Applies an ordering operation (similar to ActiveRecord #order)
168
+ # - NOTE: this option will be ignored if you need to order a DISTINCT Aggregated Array,
169
+ # since postgres will thrown an error.
170
+ #
171
+ #
172
+ #
173
+ # Examples:
116
174
  # subquery = Group.select(:name, :category_id).where("user_id = users.id")
117
175
  # User.select(:name, email).select_row_to_json(subquery, as: :users_groups, cast_as_array: true)
118
176
  # #=> [<#User name:.., email:.., users_groups: [{ name: .., category_id: .. }, ..]]
@@ -123,8 +181,74 @@ module ActiveRecordExtended
123
181
  # User.select_row_to_json(subquery, key: :group, cast_as_array: true) do |scope|
124
182
  # scope.where(group: { name: "Nerd Core" })
125
183
  # end
184
+ # #=> ```sql
185
+ # SELECT ARRAY(
186
+ # SELECT ROW_TO_JSON("group")
187
+ # FROM(SELECT name, category_id FROM groups) AS group
188
+ # WHERE group.name = 'Nerd Core'
189
+ # )
190
+ # ```
191
+ #
192
+ #
193
+ # - Array of JSONB objects
194
+ #
195
+ # subquery = Group.select(:name, :category_id)
196
+ # User.select_row_to_json(subquery, key: :group, cast_with: [:array, :to_jsonb]) do |scope|
197
+ # scope.where(group: { name: "Nerd Core" })
198
+ # end
199
+ # #=> ```sql
200
+ # SELECT ARRAY(
201
+ # SELECT TO_JSONB(ROW_TO_JSON("group"))
202
+ # FROM(SELECT name, category_id FROM groups) AS group
203
+ # WHERE group.name = 'Nerd Core'
204
+ # )
205
+ # ```
206
+ #
207
+ # - Distinct Aggregated Array
208
+ #
209
+ # subquery = Group.select(:name, :category_id)
210
+ # User.select_row_to_json(subquery, key: :group, cast_with: [:array_agg, :distinct]) do |scope|
211
+ # scope.where(group: { name: "Nerd Core" })
212
+ # end
213
+ # #=> ```sql
214
+ # SELECT ARRAY_AGG(DISTINCT (
215
+ # SELECT TO_JSONB(ROW_TO_JSON("group"))
216
+ # FROM(SELECT name, category_id FROM groups) AS group
217
+ # WHERE group.name = 'Nerd Core'
218
+ # ))
219
+ # ```
220
+ #
221
+ # - Ordering a Non-aggregated Array
222
+ #
223
+ # subquery = Group.select(:name, :category_id)
224
+ # User.select_row_to_json(subquery, key: :group, cast_with: :array, order_by: { group: { name: :desc } })
225
+ # #=> ```sql
226
+ # SELECT ARRAY(
227
+ # SELECT ROW_TO_JSON("group")
228
+ # FROM(SELECT name, category_id FROM groups) AS group
229
+ # ORDER BY group.name DESC
230
+ # )
231
+ # ```
232
+ #
233
+ # - Ordering an Aggregated Array
234
+ #
235
+ # Subquery = Group.select(:name, :category_id)
236
+ # User
237
+ # .joins(:people_groups)
238
+ # .select_row_to_json(
239
+ # subquery,
240
+ # key: :group,
241
+ # cast_with: :array_agg,
242
+ # order_by: { people_groups: :category_id }
243
+ # )
244
+ # #=> ```sql
245
+ # SELECT ARRAY_AGG((
246
+ # SELECT ROW_TO_JSON("group")
247
+ # FROM(SELECT name, category_id FROM groups) AS group
248
+ # ORDER BY group.name DESC
249
+ # ) ORDER BY people_groups.category_id ASC)
250
+ # ```
126
251
  #
127
-
128
252
  def select_row_to_json(from = nil, **options, &block)
129
253
  from.is_a?(Hash) ? options.merge!(from) : options.reverse_merge!(from: from)
130
254
  options.compact!
@@ -163,21 +287,6 @@ module ActiveRecordExtended
163
287
  # .take
164
288
  # .results["gang_members"] #=> "BANG!"
165
289
  #
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
290
  def json_build_object(key, from, **options)
182
291
  options[:key] = key
183
292
  options[:from] = from