active_record_extended 1.4.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/lib/active_record_extended/active_record.rb +1 -10
  4. data/lib/active_record_extended/active_record/relation_patch.rb +16 -1
  5. data/lib/active_record_extended/arel.rb +1 -0
  6. data/lib/active_record_extended/arel/nodes.rb +22 -21
  7. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  8. data/lib/active_record_extended/query_methods/any_of.rb +5 -4
  9. data/lib/active_record_extended/query_methods/either.rb +1 -1
  10. data/lib/active_record_extended/query_methods/inet.rb +6 -2
  11. data/lib/active_record_extended/query_methods/json.rb +13 -16
  12. data/lib/active_record_extended/query_methods/select.rb +11 -10
  13. data/lib/active_record_extended/query_methods/unionize.rb +10 -4
  14. data/lib/active_record_extended/query_methods/where_chain.rb +14 -6
  15. data/lib/active_record_extended/query_methods/window.rb +4 -3
  16. data/lib/active_record_extended/query_methods/with_cte.rb +102 -35
  17. data/lib/active_record_extended/utilities/order_by.rb +9 -28
  18. data/lib/active_record_extended/utilities/support.rb +8 -15
  19. data/lib/active_record_extended/version.rb +1 -1
  20. data/spec/query_methods/any_of_spec.rb +2 -2
  21. data/spec/query_methods/json_spec.rb +5 -5
  22. data/spec/query_methods/select_spec.rb +13 -13
  23. data/spec/query_methods/unionize_spec.rb +5 -5
  24. data/spec/query_methods/with_cte_spec.rb +12 -2
  25. data/spec/spec_helper.rb +1 -1
  26. data/spec/sql_inspections/any_of_sql_spec.rb +2 -2
  27. data/spec/sql_inspections/contains_sql_queries_spec.rb +8 -8
  28. data/spec/sql_inspections/either_sql_spec.rb +3 -3
  29. data/spec/sql_inspections/json_sql_spec.rb +0 -1
  30. data/spec/sql_inspections/unionize_sql_spec.rb +2 -2
  31. data/spec/sql_inspections/window_sql_spec.rb +12 -0
  32. data/spec/sql_inspections/with_cte_sql_spec.rb +30 -1
  33. metadata +18 -20
  34. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
  35. data/lib/active_record_extended/patch/5_0/regex_match.rb +0 -10
@@ -54,10 +54,10 @@ module ActiveRecordExtended
54
54
  elsif column.try(:array)
55
55
  Arel::Nodes::ContainsArray.new(arel.left, arel.right)
56
56
  else
57
- raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
57
+ raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
58
58
  end
59
59
  else
60
- raise ArgumentError, "Invalid argument for .where.contains(), got #{arel.class}"
60
+ raise ArgumentError.new("Invalid argument for .where.contains(), got #{arel.class}")
61
61
  end
62
62
  end
63
63
  end
@@ -88,7 +88,7 @@ module ActiveRecordExtended
88
88
  when Arel::Nodes::Equality
89
89
  Arel::Nodes::Equality.new(arel.right, Arel::Nodes::NamedFunction.new(function_name, [arel.left]))
90
90
  else
91
- raise ArgumentError, "Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}"
91
+ raise ArgumentError.new("Invalid argument for .where.#{function_name.downcase}(), got #{arel.class}")
92
92
  end
93
93
  end
94
94
  end
@@ -99,10 +99,18 @@ module ActiveRecordExtended
99
99
  when Arel::Nodes::In, Arel::Nodes::Equality
100
100
  arel_node_class.new(arel.left, arel.right)
101
101
  else
102
- raise ArgumentError, "Invalid argument for .where.#{method}(), got #{arel.class}"
102
+ raise ArgumentError.new("Invalid argument for .where.#{method}(), got #{arel.class}")
103
103
  end
104
104
  end
105
105
  end
106
+
107
+ def build_where_clause_for(scope, opts, rest)
108
+ if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1
109
+ scope.send(:build_where_clause, opts, rest)
110
+ else
111
+ scope.send(:where_clause_factory).build(opts, rest)
112
+ end
113
+ end
106
114
  end
107
115
  end
108
116
 
@@ -112,9 +120,9 @@ module ActiveRecord
112
120
  prepend ActiveRecordExtended::WhereChain
113
121
 
114
122
  def build_where_chain(opts, rest, &block)
115
- where_clause = @scope.send(:where_clause_factory).build(opts, rest)
123
+ where_clause = build_where_clause_for(@scope, opts, rest)
116
124
  @scope.tap do |scope|
117
- scope.references!(PredicateBuilder.references(opts)) if opts.is_a?(Hash)
125
+ scope.references!(PredicateBuilder.references(opts.stringify_keys)) if opts.is_a?(Hash)
118
126
  scope.where_clause += where_clause.modified_predicates(&block)
119
127
  end
120
128
  end
@@ -16,7 +16,7 @@ module ActiveRecordExtended
16
16
  @scope.window_values! << {
17
17
  window_name: to_arel_sql(@window_name),
18
18
  partition_by: flatten_to_sql(partitions),
19
- order_by: order_by_expression(order_by),
19
+ order_by: order_by_expression(order_by)
20
20
  }
21
21
 
22
22
  @scope
@@ -81,8 +81,9 @@ module ActiveRecordExtended
81
81
 
82
82
  def build_windows(arel)
83
83
  window_values.each do |window_value|
84
- window = arel.window(window_value[:window_name]).partition(window_value[:partition_by])
85
- window.order(window_value[:order_by]) if window_value[:order_by]
84
+ window = arel.window(window_value[:window_name])
85
+ window = window.partition(window_value[:partition_by]) if window_value[:partition_by].present?
86
+ window.order(window_value[:order_by]) if window_value[:order_by]
86
87
  end
87
88
  end
88
89
  end
@@ -3,78 +3,145 @@
3
3
  module ActiveRecordExtended
4
4
  module QueryMethods
5
5
  module WithCTE
6
- class WithChain
6
+ class WithCTE
7
+ include ::ActiveRecordExtended::Utilities::Support
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ def_delegators :@with_values, :empty?, :blank?, :present?
12
+ attr_reader :with_values, :with_keys
13
+
14
+ # @param [ActiveRecord::Relation] scope
7
15
  def initialize(scope)
8
16
  @scope = scope
17
+ reset!
18
+ end
19
+
20
+ # @return [Enumerable] Returns the order for which CTE's were imported as.
21
+ def each
22
+ return to_enum(:each) unless block_given?
23
+
24
+ with_keys.each do |key|
25
+ yield(key, with_values[key])
26
+ end
27
+ end
28
+ alias each_pair each
29
+
30
+ # @param [Hash, WithCTE] value
31
+ def with_values=(value)
32
+ reset!
33
+ pipe_cte_with!(value)
9
34
  end
10
35
 
11
- def recursive(*args)
36
+ # @param [Hash, WithCTE] value
37
+ def pipe_cte_with!(value)
38
+ return if value.nil? || value.empty?
39
+
40
+ value.each_pair do |name, expression|
41
+ sym_name = name.to_sym
42
+ next if with_values.key?(sym_name)
43
+
44
+ # Ensure we follow FIFO pattern.
45
+ # If the parent has similar CTE alias keys, we want to favor the parent's expressions over its children's.
46
+ if expression.is_a?(ActiveRecord::Relation) && expression.with_values?
47
+ pipe_cte_with!(expression.cte)
48
+ expression.cte.reset!
49
+ end
50
+
51
+ @with_keys |= [sym_name]
52
+ @with_values[sym_name] = expression
53
+ end
54
+
55
+ value.reset! if value.is_a?(WithCTE)
56
+ end
57
+
58
+ def reset!
59
+ @with_keys = []
60
+ @with_values = {}
61
+ end
62
+ end
63
+
64
+ class WithChain
65
+ # @param [ActiveRecord::Relation] scope
66
+ def initialize(scope)
67
+ @scope = scope
68
+ @scope.cte ||= WithCTE.new(scope)
69
+ end
70
+
71
+ # @param [Hash, WithCTE] args
72
+ def recursive(args)
12
73
  @scope.tap do |scope|
13
- scope.with_values += args
14
74
  scope.recursive_value = true
75
+ scope.cte.pipe_cte_with!(args)
15
76
  end
16
77
  end
17
78
  end
18
79
 
19
- def with_values
20
- @values[:with] || []
80
+ # @return [WithCTE]
81
+ def cte
82
+ @values[:cte]
83
+ end
84
+
85
+ # @param [WithCTE] cte
86
+ def cte=(cte)
87
+ raise TypeError.new("Must be a WithCTE class type") unless cte.is_a?(WithCTE)
88
+
89
+ @values[:cte] = cte
21
90
  end
22
91
 
92
+ # @return [Boolean]
23
93
  def with_values?
24
- !(@values[:with].nil? || @values[:with].empty?)
94
+ !(cte.nil? || cte.empty?)
25
95
  end
26
96
 
97
+ # @param [Hash, WithCTE] values
27
98
  def with_values=(values)
28
- @values[:with] = values
99
+ cte.with_values = values
29
100
  end
30
101
 
102
+ # @param [Boolean] value
31
103
  def recursive_value=(value)
32
104
  raise ImmutableRelation if @loaded
105
+
33
106
  @values[:recursive] = value
34
107
  end
35
108
 
36
- def recursive_value
37
- @values[:recursive]
109
+ # @return [Boolean]
110
+ def recursive_value?
111
+ !(!@values[:recursive])
38
112
  end
39
- alias recursive_value? recursive_value
40
113
 
114
+ # @param [Hash, WithCTE] opts
41
115
  def with(opts = :chain, *rest)
42
116
  return WithChain.new(spawn) if opts == :chain
117
+
43
118
  opts.blank? ? self : spawn.with!(opts, *rest)
44
119
  end
45
120
 
46
- def with!(opts = :chain, *rest)
121
+ # @param [Hash, WithCTE] opts
122
+ def with!(opts = :chain, *_rest)
47
123
  return WithChain.new(self) if opts == :chain
48
- self.with_values += [opts] + rest
49
- self
50
- end
51
124
 
52
- def build_with_hashed_value(with_value)
53
- with_value.map do |name, expression|
54
- select =
55
- case expression
56
- when String
57
- Arel.sql("(#{expression})")
58
- when ActiveRecord::Relation, Arel::SelectManager
59
- Arel.sql("(#{expression.to_sql})")
60
- end
61
- next if select.nil?
62
- Arel::Nodes::As.new(Arel.sql(PG::Connection.quote_ident(name.to_s)), select)
125
+ tap do |scope|
126
+ scope.cte ||= WithCTE.new(self)
127
+ scope.cte.pipe_cte_with!(opts)
63
128
  end
64
129
  end
65
130
 
66
131
  def build_with(arel)
67
- with_statements = with_values.flat_map do |with_value|
68
- case with_value
69
- when String, Arel::Nodes::As
70
- with_value
71
- when Hash
72
- build_with_hashed_value(with_value)
73
- end
74
- end.compact
132
+ return unless with_values?
133
+
134
+ cte_statements = cte.map do |name, expression|
135
+ grouped_expression = cte.generate_grouping(expression)
136
+ cte_name = cte.to_arel_sql(cte.double_quote(name.to_s))
137
+ Arel::Nodes::As.new(cte_name, grouped_expression)
138
+ end
75
139
 
76
- return if with_statements.empty?
77
- recursive_value? ? arel.with(:recursive, with_statements) : arel.with(with_statements)
140
+ if recursive_value?
141
+ arel.with(:recursive, cte_statements)
142
+ else
143
+ arel.with(cte_statements)
144
+ end
78
145
  end
79
146
  end
80
147
  end
@@ -60,34 +60,15 @@ module ActiveRecordExtended
60
60
  end
61
61
  end
62
62
 
63
- # We'll need to preprocess these arguments for allowing `ActiveRecord::Relation#preprocess_order_args`,
64
- # to check for sanitization issues and convert over to `Arel::Nodes::[Ascending/Descending]`.
65
- # Without reflecting / prepending the parent's table name.
66
- #
67
- if ActiveRecord.gem_version < Gem::Version.new("5.1")
68
- # TODO: Rails 5.0.x order logic will *always* append the parents name to the column when its an HASH obj
69
- # We should really do this stuff better. Maybe even just ignore `preprocess_order_args` altogether?
70
- # Maybe I'm just stupidly over paranoid on just the 'ORDER BY' for some odd reason.
71
- def process_ordering_arguments!(ordering_args)
72
- ordering_args.flatten!
73
- ordering_args.compact!
74
- ordering_args.map! do |arg|
75
- next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
76
- arg.each_with_object([]) do |(field, dir), ordering_object|
77
- ordering_object << to_arel_sql(field).send(dir.to_s.downcase)
78
- end
79
- end.flatten!
80
- end
81
- else
82
- def process_ordering_arguments!(ordering_args)
83
- ordering_args.flatten!
84
- ordering_args.compact!
85
- ordering_args.map! do |arg|
86
- next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
87
- arg.each_with_object({}) do |(field, dir), ordering_obj|
88
- # ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
89
- ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
90
- end
63
+ def process_ordering_arguments!(ordering_args)
64
+ ordering_args.flatten!
65
+ ordering_args.compact!
66
+ ordering_args.map! do |arg|
67
+ next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol
68
+
69
+ arg.each_with_object({}) do |(field, dir), ordering_obj|
70
+ # ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
71
+ ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
91
72
  end
92
73
  end
93
74
  end
@@ -20,7 +20,7 @@ module ActiveRecordExtended
20
20
 
21
21
  def flatten_safely(values, &block)
22
22
  unless values.is_a?(Array)
23
- values = yield values if block_given?
23
+ values = yield values if block
24
24
  return [values]
25
25
  end
26
26
 
@@ -91,20 +91,12 @@ module ActiveRecordExtended
91
91
  def pipe_cte_with!(subquery)
92
92
  return self unless subquery.try(:with_values?)
93
93
 
94
- cte_ary = flatten_safely(subquery.with_values)
95
- subquery.with_values = nil # Remove nested queries with values
96
-
97
- # Add subquery's CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
94
+ # Add subquery CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
98
95
  if @scope.with_values?
99
- # combine top-level and lower level queries `.with` values into 1 structure
100
- with_hash = cte_ary.each_with_object(@scope.with_values.first) do |from_cte, hash|
101
- hash.reverse_merge!(from_cte)
102
- end
103
-
104
- @scope.with_values = [with_hash]
96
+ @scope.cte.pipe_cte_with!(subquery.cte)
105
97
  else
106
98
  # Top level has no with values
107
- @scope.with!(*cte_ary)
99
+ @scope.with!(subquery.cte)
108
100
  end
109
101
 
110
102
  self
@@ -143,13 +135,13 @@ module ActiveRecordExtended
143
135
  # Converts a potential subquery into a compatible Arel SQL node.
144
136
  #
145
137
  # Note:
146
- # We convert relations to SQL to maintain compatibility with Rails 5.[0/1].
138
+ # We convert relations to SQL to maintain compatibility with Rails 5.1.
147
139
  # Only Rails 5.2+ maintains bound attributes in Arel, so its better to be safe then sorry.
148
- # When we drop support for Rails 5.[0/1], we then can then drop the '.to_sql' conversation
140
+ # When we drop support for Rails 5.1, we then can then drop the '.to_sql' conversation
149
141
 
150
142
  def to_arel_sql(value)
151
143
  case value
152
- when Arel::Node, Arel::Nodes::SqlLiteral, nil
144
+ when Arel::Nodes::Node, Arel::Nodes::SqlLiteral, nil
153
145
  value
154
146
  when ActiveRecord::Relation
155
147
  Arel.sql(value.spawn.to_sql)
@@ -160,6 +152,7 @@ module ActiveRecordExtended
160
152
 
161
153
  def group_when_needed(arel_or_rel_query)
162
154
  return arel_or_rel_query unless needs_to_be_grouped?(arel_or_rel_query)
155
+
163
156
  generate_grouping(arel_or_rel_query)
164
157
  end
165
158
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "1.4.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -61,7 +61,7 @@ RSpec.describe "Active Record Any / None of Methods" do
61
61
  it "Return matched records of a joined table on the parent level" do
62
62
  query = Tag.joins(:user).where.any_of(
63
63
  { users: { personal_id: 1 } },
64
- { users: { personal_id: 3 } },
64
+ { users: { personal_id: 3 } }
65
65
  )
66
66
 
67
67
  expect(query).to include(tag_one, tag_three)
@@ -120,7 +120,7 @@ RSpec.describe "Active Record Any / None of Methods" do
120
120
  it "Return matched records of a joined table on the parent level" do
121
121
  query = Tag.joins(:user).where.none_of(
122
122
  { users: { personal_id: 1 } },
123
- { users: { personal_id: 3 } },
123
+ { users: { personal_id: 3 } }
124
124
  )
125
125
 
126
126
  expect(query).to include(tag_two)
@@ -47,7 +47,7 @@ RSpec.describe "Active Record JSON methods" do
47
47
  end
48
48
 
49
49
  it "allows for casting results in an aggregate-able Array function" do
50
- query = User.select(:id).select_row_to_json(sub_query, key: :tag_row, as: :results, cast_as_array: true)
50
+ query = User.select(:id).select_row_to_json(sub_query, key: :tag_row, as: :results, cast_with: :array)
51
51
  expect(query.take.results).to be_a(Array).and(be_present)
52
52
  expect(query.take.results.first).to be_a(Hash)
53
53
  end
@@ -61,14 +61,14 @@ RSpec.describe "Active Record JSON methods" do
61
61
 
62
62
  describe ".json_build_object" do
63
63
  let(:sub_query) do
64
- User.select_row_to_json(from: User.select(:id), cast_as_array: true, as: :ids).where(id: user_one.id)
64
+ User.select_row_to_json(from: User.select(:id), cast_with: :array, as: :ids).where(id: user_one.id)
65
65
  end
66
66
 
67
67
  it "defaults the column alias if one is not provided" do
68
68
  query = User.json_build_object(:personal, sub_query)
69
69
  expect(query.size).to eq(1)
70
70
  expect(query.take.results).to match(
71
- "personal" => match("ids" => match_array([{ "id" => user_one.id }, { "id" => user_two.id }])),
71
+ "personal" => match("ids" => match_array([{ "id" => user_one.id }, { "id" => user_two.id }]))
72
72
  )
73
73
  end
74
74
 
@@ -98,7 +98,7 @@ RSpec.describe "Active Record JSON methods" do
98
98
  :personal,
99
99
  sub_query.where.not(id: user_one),
100
100
  value: "COALESCE(array_agg(\"personal\"), '{}')",
101
- as: :cool_dudes,
101
+ as: :cool_dudes
102
102
  )
103
103
 
104
104
  expect(query.take.cool_dudes["personal"]).to be_a(Array).and(be_empty)
@@ -110,7 +110,7 @@ RSpec.describe "Active Record JSON methods" do
110
110
  :personal,
111
111
  sub_query.where.not(id: user_one),
112
112
  value: "COALESCE(array_agg(personal), '{}')",
113
- as: :cool_dudes,
113
+ as: :cool_dudes
114
114
  )
115
115
  end.to output.to_stderr
116
116
  end
@@ -15,7 +15,7 @@ RSpec.describe "Active Record Select Methods" do
15
15
  it "can accept a subquery" do
16
16
  subquery = Tag.select("count(*)").joins("JOIN users u ON tags.user_id = u.id").where("u.ip = users.ip")
17
17
  query =
18
- User.foster_select(tag_count: [subquery, cast_with: :array_agg, distinct: true])
18
+ User.foster_select(tag_count: [subquery, { cast_with: :array_agg, distinct: true }])
19
19
  .joins(:hm_tags)
20
20
  .group(:ip)
21
21
  .take
@@ -25,8 +25,8 @@ RSpec.describe "Active Record Select Methods" do
25
25
 
26
26
  it "can be ordered" do
27
27
  query = User.foster_select(
28
- asc_ordered_numbers: [:number, cast_with: :array_agg, order_by: { number: :asc }],
29
- desc_ordered_numbers: [:number, cast_with: :array_agg, order_by: { number: :desc }],
28
+ asc_ordered_numbers: [:number, { cast_with: :array_agg, order_by: { number: :asc } }],
29
+ desc_ordered_numbers: [:number, { cast_with: :array_agg, order_by: { number: :desc } }]
30
30
  ).take
31
31
 
32
32
  expect(query.asc_ordered_numbers).to eq(number_set.to_a.sort)
@@ -50,10 +50,10 @@ RSpec.describe "Active Record Select Methods" do
50
50
 
51
51
  it "will return a boolean expression" do
52
52
  query = User.foster_select(
53
- truthly_expr: ["users.number > 0", cast_with: :bool_and],
54
- falsey_expr: ["users.number > 200", cast_with: :bool_and],
55
- other_true_expr: ["users.number > 4", cast_with: :bool_or],
56
- other_false_expr: ["users.number > 6", cast_with: :bool_or],
53
+ truthly_expr: ["users.number > 0", { cast_with: :bool_and }],
54
+ falsey_expr: ["users.number > 200", { cast_with: :bool_and }],
55
+ other_true_expr: ["users.number > 4", { cast_with: :bool_or }],
56
+ other_false_expr: ["users.number > 6", { cast_with: :bool_or }]
57
57
  ).take
58
58
 
59
59
  expect(query.truthly_expr).to be_truthy
@@ -67,19 +67,19 @@ RSpec.describe "Active Record Select Methods" do
67
67
  before { 2.times.flat_map { |i| Array.new(2) { |j| User.create!(number: (i + 1) * j + 3) } } }
68
68
 
69
69
  it "max" do
70
- query = User.foster_select(max_num: [:number, cast_with: :max]).take
70
+ query = User.foster_select(max_num: [:number, { cast_with: :max }]).take
71
71
  expect(query.max_num).to eq(5)
72
72
  end
73
73
 
74
74
  it "min" do
75
- query = User.foster_select(max_num: [:number, cast_with: :min]).take
75
+ query = User.foster_select(max_num: [:number, { cast_with: :min }]).take
76
76
  expect(query.max_num).to eq(3)
77
77
  end
78
78
 
79
79
  it "sum" do
80
80
  query = User.foster_select(
81
- num_sum: [:number, cast_with: :sum],
82
- distinct_sum: [:number, cast_with: :sum, distinct: true],
81
+ num_sum: [:number, { cast_with: :sum }],
82
+ distinct_sum: [:number, { cast_with: :sum, distinct: true }]
83
83
  ).take
84
84
 
85
85
  expect(query.num_sum).to eq(15)
@@ -88,8 +88,8 @@ RSpec.describe "Active Record Select Methods" do
88
88
 
89
89
  it "avg" do
90
90
  query = User.foster_select(
91
- num_avg: [:number, cast_with: :avg],
92
- distinct_avg: [:number, cast_with: :avg, distinct: true],
91
+ num_avg: [:number, { cast_with: :avg }],
92
+ distinct_avg: [:number, { cast_with: :avg, distinct: true }]
93
93
  ).take
94
94
 
95
95
  expect(query.num_avg).to eq(3.75)