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.
- 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 +2 -1
- data/lib/active_record_extended/query_methods/inet.rb +6 -2
- data/lib/active_record_extended/query_methods/json.rb +14 -17
- data/lib/active_record_extended/query_methods/select.rb +13 -12
- data/lib/active_record_extended/query_methods/unionize.rb +13 -7
- 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 +11 -30
- data/lib/active_record_extended/utilities/support.rb +9 -16
- 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
|
@@ -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
|
28
|
-
.tap(
|
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
|
-
|
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
|
|
@@ -172,7 +165,7 @@ module ActiveRecordExtended
|
|
172
165
|
end
|
173
166
|
|
174
167
|
def generate_named_function(function_name, *args)
|
175
|
-
args.map!(
|
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
|
@@ -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
|
|