active_record_extended 1.3.1 → 2.1.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -10
  3. data/lib/active_record_extended/active_record/relation_patch.rb +16 -1
  4. data/lib/active_record_extended/active_record.rb +2 -11
  5. data/lib/active_record_extended/arel/nodes.rb +24 -21
  6. data/lib/active_record_extended/arel/predications.rb +3 -2
  7. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  8. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +1 -1
  9. data/lib/active_record_extended/arel.rb +1 -0
  10. data/lib/active_record_extended/query_methods/any_of.rb +5 -4
  11. data/lib/active_record_extended/query_methods/either.rb +2 -1
  12. data/lib/active_record_extended/query_methods/inet.rb +6 -2
  13. data/lib/active_record_extended/query_methods/json.rb +14 -17
  14. data/lib/active_record_extended/query_methods/select.rb +13 -12
  15. data/lib/active_record_extended/query_methods/unionize.rb +13 -7
  16. data/lib/active_record_extended/query_methods/where_chain.rb +19 -8
  17. data/lib/active_record_extended/query_methods/window.rb +4 -3
  18. data/lib/active_record_extended/query_methods/with_cte.rb +104 -37
  19. data/lib/active_record_extended/utilities/order_by.rb +11 -30
  20. data/lib/active_record_extended/utilities/support.rb +9 -16
  21. data/lib/active_record_extended/version.rb +1 -1
  22. data/spec/query_methods/any_of_spec.rb +2 -2
  23. data/spec/query_methods/either_spec.rb +11 -0
  24. data/spec/query_methods/json_spec.rb +5 -5
  25. data/spec/query_methods/select_spec.rb +13 -13
  26. data/spec/query_methods/unionize_spec.rb +6 -6
  27. data/spec/query_methods/with_cte_spec.rb +14 -4
  28. data/spec/spec_helper.rb +1 -1
  29. data/spec/sql_inspections/any_of_sql_spec.rb +2 -2
  30. data/spec/sql_inspections/contains_sql_queries_spec.rb +8 -8
  31. data/spec/sql_inspections/either_sql_spec.rb +19 -3
  32. data/spec/sql_inspections/json_sql_spec.rb +0 -1
  33. data/spec/sql_inspections/unionize_sql_spec.rb +2 -2
  34. data/spec/sql_inspections/window_sql_spec.rb +12 -0
  35. data/spec/sql_inspections/with_cte_sql_spec.rb +30 -1
  36. data/spec/support/database_cleaner.rb +1 -1
  37. data/spec/support/models.rb +12 -0
  38. metadata +18 -20
  39. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
  40. data/lib/active_record_extended/patch/5_0/regex_match.rb +0 -10
@@ -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
- return WithChain.new(spawn) if opts == :chain
116
+ return WithChain.new(spawn) if :chain == opts
117
+
43
118
  opts.blank? ? self : spawn.with!(opts, *rest)
44
119
  end
45
120
 
46
- def with!(opts = :chain, *rest)
47
- return WithChain.new(self) if opts == :chain
48
- self.with_values += [opts] + rest
49
- self
50
- end
121
+ # @param [Hash, WithCTE] opts
122
+ def with!(opts = :chain, *_rest)
123
+ return WithChain.new(self) if :chain == opts
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
@@ -24,8 +24,8 @@ module ActiveRecordExtended
24
24
  return false unless order_by && order_by.presence.present?
25
25
 
26
26
  to_ordered_table_path(order_by)
27
- .tap(&method(:process_ordering_arguments!))
28
- .tap(&method(:scope_preprocess_order_args))
27
+ .tap { |order_args| process_ordering_arguments!(order_args) }
28
+ .tap { |order_args| scope_preprocess_order_args(order_args) }
29
29
  end
30
30
 
31
31
  #
@@ -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
 
@@ -172,7 +165,7 @@ module ActiveRecordExtended
172
165
  end
173
166
 
174
167
  def generate_named_function(function_name, *args)
175
- args.map!(&method(:to_arel_sql))
168
+ args.map! { |arg| to_arel_sql(arg) }
176
169
  function_name = function_name.to_s.upcase
177
170
  ::Arel::Nodes::NamedFunction.new(to_arel_sql(function_name), args)
178
171
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "1.3.1"
4
+ VERSION = "2.1.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)
@@ -23,6 +23,17 @@ RSpec.describe "Active Record Either Methods" do
23
23
  expect(query).to_not include(three)
24
24
  end
25
25
  end
26
+
27
+ context "Through association .either_joins/2" do
28
+ let!(:four) { User.create! }
29
+ let!(:group) { Group.create!(users: [four]) }
30
+
31
+ it "Should only only return records that belong to profile L or has group" do
32
+ query = User.either_joins(:profile_l, :groups)
33
+ expect(query).to include(one, four)
34
+ expect(query).to_not include(three, two)
35
+ end
36
+ end
26
37
  end
27
38
 
28
39
  describe ".either_order/2" do
@@ -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)
@@ -17,13 +17,13 @@ RSpec.describe "Active Record Union Methods" do
17
17
 
18
18
  it "should raise an error if the select statements do not align" do
19
19
  expect { misaligned_cmd.to_a }.to(
20
- raise_error(ActiveRecord::StatementInvalid, /each [[:alpha:]]+ query must have the same number of columns/),
20
+ raise_error(ActiveRecord::StatementInvalid, /each [[:alpha:]]+ query must have the same number of columns/)
21
21
  )
22
22
  end
23
23
 
24
24
  it "should raise an argument error if there are less then two union statements" do
25
25
  expect { lacking_union_cmd.to_a }.to(
26
- raise_error(ArgumentError, "You are required to provide 2 or more unions to join!"),
26
+ raise_error(ArgumentError, "You are required to provide 2 or more unions to join!")
27
27
  )
28
28
  end
29
29
  end
@@ -88,10 +88,10 @@ RSpec.describe "Active Record Union Methods" do
88
88
  query =
89
89
  User.union.intersect(
90
90
  User.select(:id, "profile_ls.likes").joins(:profile_l).where(profile_ls: { likes: 100 }),
91
- User.select(:id, "profile_ls.likes").joins(:profile_l).where("profile_ls.likes < 150"),
91
+ User.select(:id, "profile_ls.likes").joins(:profile_l).where("profile_ls.likes < 150")
92
92
  )
93
93
 
94
- expect(query.pluck(:id)).to have_attributes(size: 1).and(eq([user_one_pl.id]))
94
+ expect(query.pluck(:id)).to have_attributes(size: 1).and(eq([user_one_pl.user.id]))
95
95
  expect(query.first.likes).to eq(user_one_pl.likes)
96
96
  end
97
97
  end
@@ -129,7 +129,7 @@ RSpec.describe "Active Record Union Methods" do
129
129
  query =
130
130
  User.union.all(
131
131
  User.where(id: user_one.id),
132
- User.where(id: user_three.id),
132
+ User.where(id: user_three.id)
133
133
  ).order_union(id: :desc)
134
134
 
135
135
  expect(query).to eq([user_three, user_one])
@@ -144,7 +144,7 @@ RSpec.describe "Active Record Union Methods" do
144
144
  query =
145
145
  User.union.intersect(
146
146
  User.where("id < ?", user_three.id),
147
- User.where("id >= ?", user_one.id),
147
+ User.where("id >= ?", user_one.id)
148
148
  ).order_union(id: :desc)
149
149
 
150
150
  expect(query).to eq([user_two, user_one])
@@ -3,8 +3,8 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe "Active Record With CTE Query Methods" do
6
- let!(:user_one) { User.create! }
7
- let!(:user_two) { User.create! }
6
+ let!(:user_one) { User.create! }
7
+ let!(:user_two) { User.create! }
8
8
  let!(:profile_one) { ProfileL.create!(user_id: user_one.id, likes: 200) }
9
9
  let!(:profile_two) { ProfileL.create!(user_id: user_two.id, likes: 500) }
10
10
 
@@ -12,14 +12,14 @@ RSpec.describe "Active Record With CTE Query Methods" do
12
12
  context "when using as a standalone query" do
13
13
  it "should only return a person with less than 300 likes" do
14
14
  query = User.with(profile: ProfileL.where("likes < 300"))
15
- .joins("JOIN profile ON profile.id = users.id")
15
+ .joins("JOIN profile ON profile.user_id = users.id")
16
16
 
17
17
  expect(query).to match_array([user_one])
18
18
  end
19
19
 
20
20
  it "should return anyone with likes greater than or equal to 200" do
21
21
  query = User.with(profile: ProfileL.where("likes >= 200"))
22
- .joins("JOIN profile ON profile.id = users.id")
22
+ .joins("JOIN profile ON profile.user_id = users.id")
23
23
 
24
24
  expect(query).to match_array([user_one, user_two])
25
25
  end
@@ -35,6 +35,16 @@ RSpec.describe "Active Record With CTE Query Methods" do
35
35
 
36
36
  expect(query).to match_array([user_one])
37
37
  end
38
+
39
+ it "should contain a unique list of ordered CTE keys when merging in multiple children" do
40
+ x = User.with(profile: ProfileL.where("likes < 300"))
41
+ y = User.with(profile: ProfileL.where("likes > 400"))
42
+ z = y.merge(x).joins("JOIN profile ON profile.user_id = users.id") # Y should reject X's CTE (FIFO)
43
+ query = User.with(my_profile: z).joins("JOIN my_profile ON my_profile.id = users.id")
44
+
45
+ expect(query.cte.with_keys).to eq([:profile, :my_profile])
46
+ expect(query).to match_array([user_two])
47
+ end
38
48
  end
39
49
  end
40
50
  end
data/spec/spec_helper.rb CHANGED
@@ -13,7 +13,7 @@ end
13
13
  ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
14
14
 
15
15
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require File.expand_path(f) }
16
- Dir["#{File.dirname(__FILE__)}/**/*examples.rb"].each { |f| require f }
16
+ Dir["#{File.dirname(__FILE__)}/**/*examples.rb"].sort.each { |f| require f }
17
17
 
18
18
  RSpec.configure do |config|
19
19
  # Enable flags like --only-failures and --next-failure
@@ -5,8 +5,8 @@ require "spec_helper"
5
5
  RSpec.describe "Any / None of SQL Queries" do
6
6
  let(:equal_query) { '"users"."personal_id" = 1' }
7
7
  let(:or_query) { 'OR "users"."personal_id" = 2' }
8
- let(:equal_or) { equal_query + " " + or_query }
9
- let(:join_query) { /INNER JOIN \"tags\" ON \"tags\".\"user_id\" = \"users\".\"id/ }
8
+ let(:equal_or) { "#{equal_query} #{or_query}" }
9
+ let(:join_query) { /INNER JOIN "tags" ON "tags"."user_id" = "users"."id/ }
10
10
 
11
11
  describe "where.any_of/1" do
12
12
  it "should group different column arguments into nested or conditions" do
@@ -3,14 +3,14 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe "Contains SQL Queries" do
6
- let(:contains_array_regex) { /\"users\"\.\"tag_ids\" @> '\{1,2\}'/ }
7
- let(:contains_hstore_regex) { /\"users\"\.\"data\" @> '\"nickname\"=>"Dan"'/ }
8
- let(:contains_jsonb_regex) { /\"users\"\.\"jsonb_data\" @> '\{"nickname\":\"Dan"}'/ }
9
- let(:contained_in_array_regex) { /\"users\"\.\"tag_ids\" <@ '\{1,2\}'/ }
10
- let(:contained_in_hstore_regex) { /\"users\"\.\"data\" <@ '\"nickname\"=>"Dan"'/ }
11
- let(:contained_in_jsonb_regex) { /\"users\"\.\"jsonb_data\" <@ '\{"nickname\":\"Dan"}'/ }
12
- let(:contains_equals_regex) { /\"users\"\.\"ip\" >>= '127.0.0.1'/ }
13
- let(:equality_regex) { /\"users\"\.\"tags\" = '\{"?working"?\}'/ }
6
+ let(:contains_array_regex) { /"users"\."tag_ids" @> '\{1,2\}'/ }
7
+ let(:contains_hstore_regex) { /"users"\."data" @> '"nickname"=>"Dan"'/ }
8
+ let(:contains_jsonb_regex) { /"users"\."jsonb_data" @> '\{"nickname":"Dan"}'/ }
9
+ let(:contained_in_array_regex) { /"users"\."tag_ids" <@ '\{1,2\}'/ }
10
+ let(:contained_in_hstore_regex) { /"users"\."data" <@ '"nickname"=>"Dan"'/ }
11
+ let(:contained_in_jsonb_regex) { /"users"\."jsonb_data" <@ '\{"nickname":"Dan"}'/ }
12
+ let(:contains_equals_regex) { /"users"\."ip" >>= '127.0.0.1'/ }
13
+ let(:equality_regex) { /"users"\."tags" = '\{"?working"?\}'/ }
14
14
 
15
15
  describe ".where.contains(:column => value)" do
16
16
  it "generates the appropriate where clause for array columns" do
@@ -3,9 +3,9 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe "Either Methods SQL Queries" do
6
- let(:contains_array_regex) { /\"users\"\.\"tag_ids\" @> '\{1,2\}'/ }
7
- let(:profile_l_outer_join) { /LEFT OUTER JOIN \"profile_ls\" ON \"profile_ls\".\"user_id\" = \"users\".\"id\"/ }
8
- let(:profile_r_outer_join) { /LEFT OUTER JOIN \"profile_rs\" ON \"profile_rs\".\"user_id\" = \"users\".\"id\"/ }
6
+ let(:contains_array_regex) { /"users"\."tag_ids" @> '\{1,2\}'/ }
7
+ let(:profile_l_outer_join) { /LEFT OUTER JOIN "profile_ls" ON "profile_ls"."user_id" = "users"."id"/ }
8
+ let(:profile_r_outer_join) { /LEFT OUTER JOIN "profile_rs" ON "profile_rs"."user_id" = "users"."id"/ }
9
9
  let(:where_join_case) do
10
10
  "WHERE ((CASE WHEN profile_ls.user_id IS NULL"\
11
11
  " THEN profile_rs.user_id"\
@@ -31,6 +31,22 @@ RSpec.describe "Either Methods SQL Queries" do
31
31
  query = User.either_join(:profile_l, :profile_r).to_sql
32
32
  expect(query).to include(where_join_case)
33
33
  end
34
+
35
+ context "Through association .either_joins/2" do
36
+ let!(:four) { User.create! }
37
+ let!(:group) { Group.create!(users: [four]) }
38
+ let(:where_join_through_case) do
39
+ "WHERE ((CASE WHEN profile_ls.user_id IS NULL"\
40
+ " THEN groups_users.user_id"\
41
+ " ELSE profile_ls.user_id END) "\
42
+ "= users.id)"
43
+ end
44
+
45
+ it "Should contain a case statement that will conditionally alternative between tables" do
46
+ query = User.either_join(:profile_l, :groups).to_sql
47
+ expect(query).to include(where_join_through_case)
48
+ end
49
+ end
34
50
  end
35
51
 
36
52
  describe ".either_order/2" do
@@ -27,7 +27,6 @@ RSpec.describe "JSON Methods SQL Queries" do
27
27
  context "When adding cast_with: option" do
28
28
  it "should wrap the row_to_json expression with to_jsonb" do
29
29
  query = User.select_row_to_json(User.where(id: 10), cast_with: :to_jsonb, key: :convert_this, as: :results).to_sql
30
- puts query
31
30
  expect(query).to match_regex(/SELECT \(SELECT TO_JSONB\(ROW_TO_JSON\("convert_this"\)\) FROM \(.+\).+\) AS "results"/)
32
31
  end
33
32