active_record_extended 1.4.0 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -10
- data/lib/active_record_extended/active_record/relation_patch.rb +16 -1
- data/lib/active_record_extended/active_record.rb +2 -11
- data/lib/active_record_extended/arel/nodes.rb +24 -21
- data/lib/active_record_extended/arel/predications.rb +3 -2
- data/lib/active_record_extended/arel/sql_literal.rb +16 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +1 -1
- data/lib/active_record_extended/arel.rb +1 -0
- data/lib/active_record_extended/query_methods/any_of.rb +5 -4
- data/lib/active_record_extended/query_methods/either.rb +5 -2
- data/lib/active_record_extended/query_methods/inet.rb +6 -2
- data/lib/active_record_extended/query_methods/json.rb +13 -16
- data/lib/active_record_extended/query_methods/select.rb +11 -10
- data/lib/active_record_extended/query_methods/unionize.rb +12 -6
- data/lib/active_record_extended/query_methods/where_chain.rb +19 -8
- data/lib/active_record_extended/query_methods/window.rb +4 -3
- data/lib/active_record_extended/query_methods/with_cte.rb +104 -37
- data/lib/active_record_extended/utilities/order_by.rb +9 -28
- data/lib/active_record_extended/utilities/support.rb +8 -15
- data/lib/active_record_extended/version.rb +1 -1
- data/spec/query_methods/any_of_spec.rb +2 -2
- data/spec/query_methods/either_spec.rb +11 -0
- data/spec/query_methods/json_spec.rb +5 -5
- data/spec/query_methods/select_spec.rb +13 -13
- data/spec/query_methods/unionize_spec.rb +6 -6
- data/spec/query_methods/with_cte_spec.rb +14 -4
- data/spec/spec_helper.rb +1 -1
- data/spec/sql_inspections/any_of_sql_spec.rb +2 -2
- data/spec/sql_inspections/contains_sql_queries_spec.rb +8 -8
- data/spec/sql_inspections/either_sql_spec.rb +19 -3
- data/spec/sql_inspections/json_sql_spec.rb +0 -1
- data/spec/sql_inspections/unionize_sql_spec.rb +2 -2
- data/spec/sql_inspections/window_sql_spec.rb +12 -0
- data/spec/sql_inspections/with_cte_sql_spec.rb +30 -1
- data/spec/support/database_cleaner.rb +1 -1
- data/spec/support/models.rb +12 -0
- metadata +18 -20
- data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
- 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
|
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
|
-
|
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
|
-
|
20
|
-
|
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
|
-
!(
|
94
|
+
!(cte.nil? || cte.empty?)
|
25
95
|
end
|
26
96
|
|
97
|
+
# @param [Hash, WithCTE] values
|
27
98
|
def with_values=(values)
|
28
|
-
|
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
|
-
|
37
|
-
|
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
|
116
|
+
return WithChain.new(spawn) if :chain == opts
|
117
|
+
|
43
118
|
opts.blank? ? self : spawn.with!(opts, *rest)
|
44
119
|
end
|
45
120
|
|
46
|
-
|
47
|
-
|
48
|
-
self
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
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
|
-
|
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
|
-
|
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!(
|
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.
|
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.
|
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
|
|
@@ -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,
|
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),
|
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)
|
7
|
-
let!(:user_two)
|
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.
|
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.
|
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
|
9
|
-
let(:join_query) { /INNER JOIN
|
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) {
|
7
|
-
let(:contains_hstore_regex) {
|
8
|
-
let(:contains_jsonb_regex) {
|
9
|
-
let(:contained_in_array_regex) {
|
10
|
-
let(:contained_in_hstore_regex) {
|
11
|
-
let(:contained_in_jsonb_regex) {
|
12
|
-
let(:contains_equals_regex) {
|
13
|
-
let(:equality_regex) {
|
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) {
|
7
|
-
let(:profile_l_outer_join) { /LEFT OUTER JOIN
|
8
|
-
let(:profile_r_outer_join) { /LEFT OUTER JOIN
|
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
|
|
@@ -74,7 +74,7 @@ RSpec.describe "Union SQL Queries" do
|
|
74
74
|
|
75
75
|
it "should alias the union from clause to 'happy_users'" do
|
76
76
|
expect(described_method).to match_regex(/FROM \(+.+\) UNION \(.+\)+ happy_users$/)
|
77
|
-
expect(described_method).to match_regex(/^SELECT happy_users\.id FROM.+happy_users$/)
|
77
|
+
expect(described_method).to match_regex(/^SELECT (happy_users\.id|"happy_users"\."id") FROM.+happy_users$/)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
@@ -83,7 +83,7 @@ RSpec.describe "Union SQL Queries" do
|
|
83
83
|
|
84
84
|
it "should retain the actual class calling table name as the union alias" do
|
85
85
|
expect(described_method).to match_regex(/FROM \(+.+\) UNION \(.+\)+ users$/)
|
86
|
-
expect(described_method).to match_regex(/^SELECT
|
86
|
+
expect(described_method).to match_regex(/^SELECT "users"\."id" FROM.+users$/)
|
87
87
|
end
|
88
88
|
end
|
89
89
|
end
|