active_record_extended_telescope 2.0.1
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 +7 -0
- data/README.md +870 -0
- data/lib/active_record_extended.rb +10 -0
- data/lib/active_record_extended/active_record.rb +25 -0
- data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
- data/lib/active_record_extended/arel.rb +7 -0
- data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
- data/lib/active_record_extended/arel/nodes.rb +49 -0
- data/lib/active_record_extended/arel/predications.rb +50 -0
- data/lib/active_record_extended/arel/sql_literal.rb +16 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
- data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
- data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
- data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
- data/lib/active_record_extended/query_methods/any_of.rb +93 -0
- data/lib/active_record_extended/query_methods/either.rb +62 -0
- data/lib/active_record_extended/query_methods/inet.rb +88 -0
- data/lib/active_record_extended/query_methods/json.rb +329 -0
- data/lib/active_record_extended/query_methods/select.rb +118 -0
- data/lib/active_record_extended/query_methods/unionize.rb +249 -0
- data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
- data/lib/active_record_extended/query_methods/window.rb +93 -0
- data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
- data/lib/active_record_extended/utilities/order_by.rb +77 -0
- data/lib/active_record_extended/utilities/support.rb +178 -0
- data/lib/active_record_extended/version.rb +5 -0
- data/lib/active_record_extended_telescope.rb +4 -0
- data/spec/active_record_extended_spec.rb +7 -0
- data/spec/query_methods/any_of_spec.rb +131 -0
- data/spec/query_methods/array_query_spec.rb +64 -0
- data/spec/query_methods/either_spec.rb +59 -0
- data/spec/query_methods/hash_query_spec.rb +45 -0
- data/spec/query_methods/inet_query_spec.rb +112 -0
- data/spec/query_methods/json_spec.rb +157 -0
- data/spec/query_methods/select_spec.rb +115 -0
- data/spec/query_methods/unionize_spec.rb +165 -0
- data/spec/query_methods/window_spec.rb +51 -0
- data/spec/query_methods/with_cte_spec.rb +50 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
- data/spec/sql_inspections/arel/array_spec.rb +63 -0
- data/spec/sql_inspections/arel/inet_spec.rb +66 -0
- data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
- data/spec/sql_inspections/either_sql_spec.rb +55 -0
- data/spec/sql_inspections/json_sql_spec.rb +82 -0
- data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
- data/spec/sql_inspections/window_sql_spec.rb +98 -0
- data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
- data/spec/support/database_cleaner.rb +15 -0
- data/spec/support/models.rb +68 -0
- metadata +245 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Arel::Nodes::AggregateFunctionName do
|
6
|
+
describe "Custom Aggregate function" do
|
7
|
+
it "constructs an aggregate function based on a given name" do
|
8
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me")])
|
9
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(id == me)")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can append multiple expressions" do
|
13
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me"), Arel.sql("id == you")])
|
14
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(id == me, id == you)")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "can append a distinct clause inside the aggregate" do
|
18
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me")], true)
|
19
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(DISTINCT id == me)")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can append an order by clause when providing a ordering expression" do
|
23
|
+
order_expr = Arel.sql("id").desc
|
24
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me")], true).order_by([order_expr])
|
25
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(DISTINCT id == me ORDER BY id DESC)")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can append multiple ordering clauses" do
|
29
|
+
expr = Arel.sql("id").desc
|
30
|
+
other_expr = Arel.sql("name").asc
|
31
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me")], true).order_by([expr, other_expr])
|
32
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(DISTINCT id == me ORDER BY id DESC, name ASC)")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can be aliased" do
|
36
|
+
alias_as = Arel.sql("new_name")
|
37
|
+
query = described_class.new("MY_CUSTOM_AGG", [Arel.sql("id == me")], true).as(alias_as)
|
38
|
+
expect(query.to_sql).to eq("MY_CUSTOM_AGG(DISTINCT id == me) AS new_name")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Array Column Predicates" do
|
6
|
+
let(:arel_table) { User.arel_table }
|
7
|
+
|
8
|
+
describe "Array Overlap" do
|
9
|
+
it "converts Arel overlap statement" do
|
10
|
+
query = arel_table.where(arel_table[:tags].overlap(["tag", "tag 2"])).to_sql
|
11
|
+
expect(query).to match_regex(/&& '\{"?tag"?,"tag 2"\}'/)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "converts Arel overlap statement" do
|
15
|
+
query = arel_table.where(arel_table[:tag_ids].overlap([1, 2])).to_sql
|
16
|
+
expect(query).to match_regex(/&& '\{1,2\}'/)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "works with count (and other predicates)" do
|
20
|
+
expect(User.where(arel_table[:tag_ids].overlap([1, 2])).count).to eq 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "Array Contains" do
|
25
|
+
it "converts Arel contains statement and escapes strings" do
|
26
|
+
query = arel_table.where(arel_table[:tags].contains(["tag", "tag 2"])).to_sql
|
27
|
+
expect(query).to match_regex(/@> '\{"?tag"?,"tag 2"\}'/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "converts Arel contains statement with numbers" do
|
31
|
+
query = arel_table.where(arel_table[:tag_ids].contains([1, 2])).to_sql
|
32
|
+
expect(query).to match_regex(/@> '\{1,2\}'/)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "works with count (and other predicates)" do
|
36
|
+
expect(User.where(arel_table[:tag_ids].contains([1, 2])).count).to eq 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "Any Array Element" do
|
41
|
+
it "creates any predicates that contain a string value" do
|
42
|
+
query = arel_table.where(arel_table[:tags].any("tag")).to_sql
|
43
|
+
expect(query).to match_regex(/'tag' = ANY\("users"\."tags"\)/)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "creates any predicates that contain a integer value" do
|
47
|
+
query = arel_table.where(arel_table[:tags].any(2)).to_sql
|
48
|
+
expect(query).to match_regex(/2 = ANY\("users"\."tags"\)/)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "All Array Elements" do
|
53
|
+
it "create all predicates that contain a string value" do
|
54
|
+
query = arel_table.where(arel_table[:tags].all("tag")).to_sql
|
55
|
+
expect(query).to match_regex(/'tag' = ALL\("users"\."tags"\)/)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "create all predicates that contain a interger value" do
|
59
|
+
query = arel_table.where(arel_table[:tags].all(2)).to_sql
|
60
|
+
expect(query).to match_regex(/2 = ALL\("users"\."tags"\)/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Inet Column Predicates" do
|
6
|
+
let(:arel_table) { User.arel_table }
|
7
|
+
|
8
|
+
describe "#inet_contained_within" do
|
9
|
+
it "converts Arel inet contained within statement" do
|
10
|
+
query = arel_table.where(arel_table[:ip].inet_contained_within(IPAddr.new("127.0.0.1"))).to_sql
|
11
|
+
expect(query).to match_regex(%r{<< '127\.0\.0\.1/32'})
|
12
|
+
end
|
13
|
+
|
14
|
+
it "works with count" do
|
15
|
+
expect(User.where(arel_table[:ip].inet_contained_within(IPAddr.new("127.0.0.1"))).count).to eq(0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#inet_contained_within_or_equals" do
|
20
|
+
it "converts Arel inet contained within statement" do
|
21
|
+
query = arel_table.where(arel_table[:ip].inet_contained_within_or_equals(IPAddr.new("127.0.0.1"))).to_sql
|
22
|
+
expect(query).to match_regex(%r{<<= '127\.0\.0\.1/32'})
|
23
|
+
end
|
24
|
+
|
25
|
+
it "works with count" do
|
26
|
+
expect(User.where(arel_table[:ip].inet_contained_within_or_equals(IPAddr.new("127.0.0.1"))).count).to eq(0)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#inet_contains_or_equals" do
|
31
|
+
it "converts Arel inet contained within statement" do
|
32
|
+
query = arel_table.where(arel_table[:ip].inet_contains_or_equals(IPAddr.new("127.0.0.1"))).to_sql
|
33
|
+
expect(query).to match_regex(%r{>>= '127\.0\.0\.1/32'})
|
34
|
+
end
|
35
|
+
|
36
|
+
it "works with count" do
|
37
|
+
expect(User.where(arel_table[:ip].inet_contains_or_equals(IPAddr.new("127.0.0.1"))).count).to eq(0)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#inet_contains" do
|
42
|
+
it "converts Arel inet contained within statement" do
|
43
|
+
query = arel_table.where(arel_table[:ip].inet_contains("127.0.0.1")).to_sql
|
44
|
+
expect(query).to match_regex(/>> '127\.0\.0\.1'/)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "works with count" do
|
48
|
+
expect(User.where(arel_table[:ip].inet_contains("127.0.0.1")).count).to eq(0)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#inet_contains_or_is_contained_within" do
|
53
|
+
it "converts Arel inet contained within statement" do
|
54
|
+
query = arel_table.where(arel_table[:ip].inet_contains_or_is_contained_within("127.0.0.1")).to_sql
|
55
|
+
expect(query).to match_regex(/&& '127\.0\.0\.1'/)
|
56
|
+
|
57
|
+
query = arel_table.where(arel_table[:ip].inet_contains_or_is_contained_within(IPAddr.new("127.0.0.1"))).to_sql
|
58
|
+
expect(query).to match_regex(%r{&& '127\.0\.0\.1/32'})
|
59
|
+
end
|
60
|
+
|
61
|
+
it "works with count" do
|
62
|
+
expect(User.where(arel_table[:ip].inet_contains_or_is_contained_within("127.0.0.1")).count).to eq(0)
|
63
|
+
expect(User.where(arel_table[:ip].inet_contains_or_is_contained_within(IPAddr.new("127.0.0.1"))).count).to eq(0)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
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"?\}'/ }
|
14
|
+
|
15
|
+
describe ".where.contains(:column => value)" do
|
16
|
+
it "generates the appropriate where clause for array columns" do
|
17
|
+
query = User.where.contains(tag_ids: [1, 2]).to_sql
|
18
|
+
expect(query).to match_regex(contains_array_regex)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "generates the appropriate where clause for hstore columns" do
|
22
|
+
query = User.where.contains(data: { nickname: "Dan" }).to_sql
|
23
|
+
expect(query).to match_regex(contains_hstore_regex)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "generates the appropriate where clause for jsonb columns" do
|
27
|
+
query = User.where.contains(jsonb_data: { nickname: "Dan" }).to_sql
|
28
|
+
expect(query).to match_regex(contains_jsonb_regex)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "generates the appropriate where clause for hstore columns on joins" do
|
32
|
+
query = Tag.joins(:user).where.contains(users: { data: { nickname: "Dan" } }).to_sql
|
33
|
+
expect(query).to match_regex(contains_hstore_regex)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "allows chaining" do
|
37
|
+
query = User.where.contains(tag_ids: [1, 2]).where(tags: ["working"]).to_sql
|
38
|
+
expect(query).to match_regex(contains_array_regex)
|
39
|
+
expect(query).to match_regex(equality_regex)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "generates the appropriate where clause for array columns on joins" do
|
43
|
+
query = Tag.joins(:user).where.contains(users: { tag_ids: [1, 2] }).to_sql
|
44
|
+
expect(query).to match_regex(contains_array_regex)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
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"/ }
|
9
|
+
let(:where_join_case) do
|
10
|
+
"WHERE ((CASE WHEN profile_ls.user_id IS NULL"\
|
11
|
+
" THEN profile_rs.user_id"\
|
12
|
+
" ELSE profile_ls.user_id END) "\
|
13
|
+
"= users.id)"
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:order_case) do
|
17
|
+
"ORDER BY "\
|
18
|
+
"(CASE WHEN profile_ls.likes IS NULL"\
|
19
|
+
" THEN profile_rs.dislikes"\
|
20
|
+
" ELSE profile_ls.likes END)"
|
21
|
+
end
|
22
|
+
|
23
|
+
describe ".either_join/2" do
|
24
|
+
it "Should contain outer joins on the provided relationships" do
|
25
|
+
query = User.either_join(:profile_l, :profile_r).to_sql
|
26
|
+
expect(query).to match_regex(profile_l_outer_join)
|
27
|
+
expect(query).to match_regex(profile_r_outer_join)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "Should contain a case statement that will conditionally alternative between tables" do
|
31
|
+
query = User.either_join(:profile_l, :profile_r).to_sql
|
32
|
+
expect(query).to include(where_join_case)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe ".either_order/2" do
|
37
|
+
let(:ascended_order) { User.either_order(:asc, profile_l: :likes, profile_r: :dislikes).to_sql }
|
38
|
+
let(:descended_order) { User.either_order(:desc, profile_l: :likes, profile_r: :dislikes).to_sql }
|
39
|
+
|
40
|
+
it "Should contain outer joins on the provided relationships" do
|
41
|
+
expect(ascended_order).to match_regex(profile_l_outer_join)
|
42
|
+
expect(ascended_order).to match_regex(profile_r_outer_join)
|
43
|
+
expect(descended_order).to match_regex(profile_l_outer_join)
|
44
|
+
expect(descended_order).to match_regex(profile_r_outer_join)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "Should contain a relational ordering case statement for a relations column" do
|
48
|
+
expect(ascended_order).to include(order_case)
|
49
|
+
expect(ascended_order).to end_with("asc")
|
50
|
+
|
51
|
+
expect(descended_order).to include(order_case)
|
52
|
+
expect(descended_order).to end_with("desc")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "JSON Methods SQL Queries" do
|
6
|
+
let(:single_with) { /^WITH .all_others. AS(?!.*WITH \w?)/mi }
|
7
|
+
let(:override_with) { /^WITH .all_others. AS \(.+WHERE .users.\..id. = 10\)/mi }
|
8
|
+
|
9
|
+
describe ".select_row_to_json" do
|
10
|
+
context "when a subquery contains a CTE table" do
|
11
|
+
let(:cte_person) { User.with(all_others: User.where.not(id: 1)).where(id: 2) }
|
12
|
+
|
13
|
+
it "should push the CTE to the callee's level" do
|
14
|
+
query = User.select_row_to_json(cte_person, as: :results).to_sql
|
15
|
+
expect(query).to match_regex(single_with)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should favor the parents CTE table if names collide" do
|
19
|
+
query = User.with(all_others: User.where(id: 10))
|
20
|
+
query = query.select_row_to_json(cte_person, as: :results).to_sql
|
21
|
+
|
22
|
+
expect(query).to match_regex(single_with)
|
23
|
+
expect(query).to match_regex(override_with)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "When adding cast_with: option" do
|
28
|
+
it "should wrap the row_to_json expression with to_jsonb" do
|
29
|
+
query = User.select_row_to_json(User.where(id: 10), cast_with: :to_jsonb, key: :convert_this, as: :results).to_sql
|
30
|
+
expect(query).to match_regex(/SELECT \(SELECT TO_JSONB\(ROW_TO_JSON\("convert_this"\)\) FROM \(.+\).+\) AS "results"/)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should cast object to an array" do
|
34
|
+
query = User.select_row_to_json(User.where(id: 10), cast_with: :array, key: :convert_this, as: :results).to_sql
|
35
|
+
expect(query).to match_regex(/SELECT \(ARRAY\(SELECT ROW_TO_JSON\("convert_this"\) FROM \(.+\).+\)\) AS "results"/)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should cast object to an aggregated array" do
|
39
|
+
query = User.select_row_to_json(User.where(id: 10), cast_with: :array_agg, key: :convert_this, as: :results).to_sql
|
40
|
+
expect(query).to match_regex(/SELECT \(ARRAY_AGG\(\(SELECT TO_JSONB\(ROW_TO_JSON\("convert_this"\)\) FROM \(.+\).+\)\)\) AS "results"/)
|
41
|
+
end
|
42
|
+
|
43
|
+
context "When multiple cast_with options are used" do
|
44
|
+
it "should cast query with to_jsonb and as an Array" do
|
45
|
+
query = User.select_row_to_json(User.where(id: 10), cast_with: [:to_jsonb, :array], key: :convert_this, as: :results).to_sql
|
46
|
+
expect(query).to match_regex(/SELECT \(ARRAY\(SELECT TO_JSONB\(ROW_TO_JSON\("convert_this"\)\) FROM \(.+\).+\)\) AS "results"/)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should cast query as a distinct Aggregated Array" do
|
50
|
+
query = User.select_row_to_json(User.where(id: 10), cast_with: [:array_agg, :distinct], key: :convert_this, as: :results).to_sql
|
51
|
+
expect(query).to match_regex(/SELECT \(ARRAY_AGG\(DISTINCT \(SELECT TO_JSONB\(ROW_TO_JSON\("convert_this"\)\) FROM \(.+\).+\)\)\) AS "results"/)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when the subquery is a STI record type" do
|
57
|
+
it "should not append sti 'type IN(..)' where clauses to the nested query" do
|
58
|
+
query = User.select_row_to_json(AdminSti.where(id: 10), cast_with: :array, key: :convert_this, as: :results).to_sql
|
59
|
+
expect(query).to match_regex(/SELECT \(ARRAY\(SELECT ROW_TO_JSON\("convert_this"\) FROM \(.*\) convert_this\)\) AS .+/)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe ".json_build_object" do
|
65
|
+
context "when a subquery contains a CTE table" do
|
66
|
+
let(:cte_person) { User.with(all_others: User.where.not(id: 1)).where(id: 2) }
|
67
|
+
|
68
|
+
it "should push the CTE to the callee's level" do
|
69
|
+
query = User.json_build_object(:userss, cte_person).to_sql
|
70
|
+
expect(query).to match_regex(single_with)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should favor the parents CTE table if names collide" do
|
74
|
+
query = User.with(all_others: User.where(id: 10))
|
75
|
+
query = query.json_build_object(:users, cte_person).to_sql
|
76
|
+
|
77
|
+
expect(query).to match_regex(single_with)
|
78
|
+
expect(query).to match_regex(override_with)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Union SQL Queries" do
|
6
|
+
let(:user) { User.where(id: 1) }
|
7
|
+
let(:other_user) { User.where("id = 2") }
|
8
|
+
|
9
|
+
shared_examples_for "unions" do
|
10
|
+
it { is_expected.to eq("( (#{user.to_sql}) #{described_union} (#{other_user.to_sql}) )") }
|
11
|
+
end
|
12
|
+
|
13
|
+
shared_examples_for "piping nest CTE tables" do
|
14
|
+
let(:cte_user) { User.with(all_others: User.where.not(id: 1)).where(id: 2) }
|
15
|
+
let(:method) { raise "Required to override this method!" }
|
16
|
+
let(:single_with) { /^WITH .all_others. AS(?!.*WITH \w?)/mi }
|
17
|
+
let(:override_with) { /^WITH .all_others. AS \(.+WHERE .users.\..id. = 10\)/mi }
|
18
|
+
|
19
|
+
it "should push the CTE to the callee's level" do
|
20
|
+
query = User.send(method.to_sym, cte_user, other_user).to_sql
|
21
|
+
expect(query).to match_regex(single_with)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should favor the parents CTE table if names collide" do
|
25
|
+
query = User.with(all_others: User.where(id: 10))
|
26
|
+
query = query.send(method.to_sym, cte_user, other_user).to_sql
|
27
|
+
|
28
|
+
expect(query).to match_regex(single_with)
|
29
|
+
expect(query).to match_regex(override_with)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".union" do
|
34
|
+
let!(:described_union) { "UNION" }
|
35
|
+
subject(:described_method) { User.union(user, other_user).to_union_sql }
|
36
|
+
it_behaves_like "unions"
|
37
|
+
it_behaves_like "piping nest CTE tables" do
|
38
|
+
let!(:method) { :union }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ".union.all" do
|
43
|
+
let!(:described_union) { "UNION ALL" }
|
44
|
+
subject(:described_method) { User.union.all(user, other_user).to_union_sql }
|
45
|
+
it_behaves_like "unions"
|
46
|
+
it_behaves_like "piping nest CTE tables" do
|
47
|
+
let!(:method) { :union_all }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".union.except" do
|
52
|
+
let!(:described_union) { "EXCEPT" }
|
53
|
+
subject(:described_method) { User.union.except(user, other_user).to_union_sql }
|
54
|
+
it_behaves_like "unions"
|
55
|
+
it_behaves_like "piping nest CTE tables" do
|
56
|
+
let!(:method) { :union_except }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "union.intersect" do
|
61
|
+
let!(:described_union) { "INTERSECT" }
|
62
|
+
subject(:described_method) { User.union.intersect(user, other_user).to_union_sql }
|
63
|
+
it_behaves_like "unions"
|
64
|
+
it_behaves_like "piping nest CTE tables" do
|
65
|
+
let!(:method) { :union_intersect }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "union.as" do
|
70
|
+
context "when a union.as has been called" do
|
71
|
+
subject(:described_method) do
|
72
|
+
User.select("happy_users.id").union(user, other_user).union.as(:happy_users).to_sql
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should alias the union from clause to 'happy_users'" do
|
76
|
+
expect(described_method).to match_regex(/FROM \(+.+\) UNION \(.+\)+ happy_users$/)
|
77
|
+
expect(described_method).to match_regex(/^SELECT (happy_users\.id|"happy_users"\."id") FROM.+happy_users$/)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "when user.as hasn't been called" do
|
82
|
+
subject(:described_method) { User.select(:id).union(user, other_user).to_sql }
|
83
|
+
|
84
|
+
it "should retain the actual class calling table name as the union alias" do
|
85
|
+
expect(described_method).to match_regex(/FROM \(+.+\) UNION \(.+\)+ users$/)
|
86
|
+
expect(described_method).to match_regex(/^SELECT "users"\."id" FROM.+users$/)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "union.order" do
|
92
|
+
context "when rendering with .to_union_sql" do
|
93
|
+
subject(:described_method) { User.union(user, other_user).union.order(:id, name: :desc).to_union_sql }
|
94
|
+
|
95
|
+
it "Should append an 'ORDER BY' to the end of the union statements" do
|
96
|
+
expect(described_method).to match_regex(/^\(+.+\) UNION \(.+\) \) ORDER BY id, name DESC$/)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when rendering with .to_sql" do
|
101
|
+
subject(:described_method) { User.union(user, other_user).union.order(:id, name: :desc).to_sql }
|
102
|
+
|
103
|
+
it "Should append an 'ORDER BY' to the end of the union statements" do
|
104
|
+
expect(described_method).to match_regex(/FROM \(+.+\) UNION \(.+\) \) ORDER BY id, name DESC\) users$/)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "when a there are multiple union statements" do
|
109
|
+
let(:query_regex) { /(?<=\)\s(ORDER BY)) id/ }
|
110
|
+
|
111
|
+
it "should only append an order by to the very end of a union statements" do
|
112
|
+
query = User.union.order(id: :asc, tags: :desc)
|
113
|
+
.union(user.order(id: :asc, tags: :desc))
|
114
|
+
.union(user.order(:id, :tags))
|
115
|
+
.union(other_user.order(id: :desc, tags: :desc))
|
116
|
+
.to_union_sql
|
117
|
+
|
118
|
+
index = query.index(query_regex)
|
119
|
+
expect(index).to be_truthy
|
120
|
+
expect(query[index..-1]).to eq(" id ASC, tags DESC")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|