active_record_extended 1.3.1 → 2.1.0

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