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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +870 -0
  3. data/lib/active_record_extended.rb +10 -0
  4. data/lib/active_record_extended/active_record.rb +25 -0
  5. data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
  6. data/lib/active_record_extended/arel.rb +7 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +49 -0
  9. data/lib/active_record_extended/arel/predications.rb +50 -0
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
  12. data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
  13. data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
  14. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
  15. data/lib/active_record_extended/query_methods/any_of.rb +93 -0
  16. data/lib/active_record_extended/query_methods/either.rb +62 -0
  17. data/lib/active_record_extended/query_methods/inet.rb +88 -0
  18. data/lib/active_record_extended/query_methods/json.rb +329 -0
  19. data/lib/active_record_extended/query_methods/select.rb +118 -0
  20. data/lib/active_record_extended/query_methods/unionize.rb +249 -0
  21. data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
  22. data/lib/active_record_extended/query_methods/window.rb +93 -0
  23. data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
  24. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  25. data/lib/active_record_extended/utilities/support.rb +178 -0
  26. data/lib/active_record_extended/version.rb +5 -0
  27. data/lib/active_record_extended_telescope.rb +4 -0
  28. data/spec/active_record_extended_spec.rb +7 -0
  29. data/spec/query_methods/any_of_spec.rb +131 -0
  30. data/spec/query_methods/array_query_spec.rb +64 -0
  31. data/spec/query_methods/either_spec.rb +59 -0
  32. data/spec/query_methods/hash_query_spec.rb +45 -0
  33. data/spec/query_methods/inet_query_spec.rb +112 -0
  34. data/spec/query_methods/json_spec.rb +157 -0
  35. data/spec/query_methods/select_spec.rb +115 -0
  36. data/spec/query_methods/unionize_spec.rb +165 -0
  37. data/spec/query_methods/window_spec.rb +51 -0
  38. data/spec/query_methods/with_cte_spec.rb +50 -0
  39. data/spec/spec_helper.rb +28 -0
  40. data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  42. data/spec/sql_inspections/arel/array_spec.rb +63 -0
  43. data/spec/sql_inspections/arel/inet_spec.rb +66 -0
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
  45. data/spec/sql_inspections/either_sql_spec.rb +55 -0
  46. data/spec/sql_inspections/json_sql_spec.rb +82 -0
  47. data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
  48. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
  50. data/spec/support/database_cleaner.rb +15 -0
  51. data/spec/support/models.rb +68 -0
  52. 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