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,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Active Record Select Methods" do
6
+ let(:numbers) { (1..10).to_a }
7
+
8
+ describe ".foster_select" do
9
+ context "with an aggregate function" do
10
+ context "agg_array" do
11
+ let(:number_set) { numbers.sample(6).to_enum }
12
+ let!(:users) { Array.new(6) { User.create!(number: number_set.next, ip: "127.0.0.1") } }
13
+ let!(:tags) { users.flat_map { |u| Array.new(2) { Tag.create!(user: u, tag_number: numbers.sample) } } }
14
+
15
+ it "can accept a subquery" do
16
+ subquery = Tag.select("count(*)").joins("JOIN users u ON tags.user_id = u.id").where("u.ip = users.ip")
17
+ query =
18
+ User.foster_select(tag_count: [subquery, { cast_with: :array_agg, distinct: true }])
19
+ .joins(:hm_tags)
20
+ .group(:ip)
21
+ .take
22
+
23
+ expect(query.tag_count).to eq([tags.size])
24
+ end
25
+
26
+ it "can be ordered" do
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 } }]
30
+ ).take
31
+
32
+ expect(query.asc_ordered_numbers).to eq(number_set.to_a.sort)
33
+ expect(query.desc_ordered_numbers).to eq(number_set.to_a.sort.reverse)
34
+ end
35
+
36
+ it "works with joined relations" do
37
+ query =
38
+ User.foster_select(tag_numbers: { tags: :tag_number, cast_with: :array_agg })
39
+ .joins(:hm_tags)
40
+ .take
41
+ expect(query.tag_numbers).to match_array(Tag.pluck(:tag_number))
42
+ end
43
+ end
44
+
45
+ context "bool_[and|or]" do
46
+ let!(:users) do
47
+ enum_numbers = numbers.to_enum
48
+ Array.new(6) { User.create!(number: enum_numbers.next, ip: "127.0.0.1") }
49
+ end
50
+
51
+ it "will return a boolean expression" do
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 }]
57
+ ).take
58
+
59
+ expect(query.truthly_expr).to be_truthy
60
+ expect(query.falsey_expr).to be_falsey
61
+ expect(query.other_true_expr).to be_truthy
62
+ expect(query.other_false_expr).to be_falsey
63
+ end
64
+ end
65
+
66
+ context "with math functions: sum|max|min|avg" do
67
+ before { 2.times.flat_map { |i| Array.new(2) { |j| User.create!(number: (i + 1) * j + 3) } } }
68
+
69
+ it "max" do
70
+ query = User.foster_select(max_num: [:number, { cast_with: :max }]).take
71
+ expect(query.max_num).to eq(5)
72
+ end
73
+
74
+ it "min" do
75
+ query = User.foster_select(max_num: [:number, { cast_with: :min }]).take
76
+ expect(query.max_num).to eq(3)
77
+ end
78
+
79
+ it "sum" do
80
+ query = User.foster_select(
81
+ num_sum: [:number, { cast_with: :sum }],
82
+ distinct_sum: [:number, { cast_with: :sum, distinct: true }]
83
+ ).take
84
+
85
+ expect(query.num_sum).to eq(15)
86
+ expect(query.distinct_sum).to eq(12)
87
+ end
88
+
89
+ it "avg" do
90
+ query = User.foster_select(
91
+ num_avg: [:number, { cast_with: :avg }],
92
+ distinct_avg: [:number, { cast_with: :avg, distinct: true }]
93
+ ).take
94
+
95
+ expect(query.num_avg).to eq(3.75)
96
+ expect(query.distinct_avg).to eq(4.0)
97
+ end
98
+ end
99
+ end
100
+
101
+ context "with standard select items" do
102
+ let!(:user) { User.create!(name: "Test") }
103
+
104
+ it "works with no alias" do
105
+ query = User.foster_select(:name).take
106
+ expect(query.name).to eq(user.name)
107
+ end
108
+
109
+ it "works with alias" do
110
+ query = User.foster_select(my_name: :name).take
111
+ expect(query.my_name).to eq(user.name)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Active Record Union Methods" do
6
+ let!(:user_one) { User.create!(number: 8) }
7
+ let!(:user_two) { User.create!(number: 10) }
8
+ let!(:user_three) { User.create!(number: 1) }
9
+ let!(:user_one_pl) { ProfileL.create!(user: user_one, likes: 100) }
10
+ let!(:user_two_pl) { ProfileL.create!(user: user_two, likes: 200) }
11
+
12
+ shared_examples_for "standard set of errors" do
13
+ let(:user_one_query) { User.select(:id).where(id: user_one.id) }
14
+ let(:user_two_query) { User.select(:id, :tags).where(id: user_two.id) }
15
+ let(:misaligned_cmd) { raise("required to override this 'let' statement") }
16
+ let(:lacking_union_cmd) { raise("required to override this 'let' statement") }
17
+
18
+ it "should raise an error if the select statements do not align" do
19
+ expect { misaligned_cmd.to_a }.to(
20
+ raise_error(ActiveRecord::StatementInvalid, /each [[:alpha:]]+ query must have the same number of columns/)
21
+ )
22
+ end
23
+
24
+ it "should raise an argument error if there are less then two union statements" do
25
+ expect { lacking_union_cmd.to_a }.to(
26
+ raise_error(ArgumentError, "You are required to provide 2 or more unions to join!")
27
+ )
28
+ end
29
+ end
30
+
31
+ describe ".union" do
32
+ it_behaves_like "standard set of errors" do
33
+ let!(:misaligned_cmd) { User.union(user_one_query, user_two_query) }
34
+ let!(:lacking_union_cmd) { User.union(user_one_query) }
35
+ end
36
+
37
+ it "should return two users that match the where conditions" do
38
+ query = User.union(User.where(id: user_one.id), User.where(id: user_three.id))
39
+ expect(query).to match_array([user_one, user_three])
40
+ end
41
+
42
+ it "should allow joins on union statements" do
43
+ query = User.union(User.where(id: user_one.id), User.joins(:profile_l).where.not(id: user_one.id))
44
+ expect(query).to match_array([user_one, user_two])
45
+ end
46
+
47
+ it "should eliminate duplicate results" do
48
+ expected_ids = User.pluck(:id)
49
+ query = User.union(User.select(:id), User.select(:id))
50
+ expect(query.pluck(:id)).to have_attributes(size: expected_ids.size).and(match_array(expected_ids))
51
+ end
52
+ end
53
+
54
+ describe ".union.all" do
55
+ it_behaves_like "standard set of errors" do
56
+ let!(:misaligned_cmd) { User.union.all(user_one_query, user_two_query) }
57
+ let!(:lacking_union_cmd) { User.union.all(user_one_query) }
58
+ end
59
+
60
+ it "should keep duplicate results from each union statement" do
61
+ expected_ids = User.pluck(:id) * 2
62
+ query = User.union.all(User.select(:id), User.select(:id))
63
+ expect(query.pluck(:id)).to have_attributes(size: expected_ids.size).and(match_array(expected_ids))
64
+ end
65
+ end
66
+
67
+ describe ".union.except" do
68
+ it_behaves_like "standard set of errors" do
69
+ let!(:misaligned_cmd) { User.union.except(user_one_query, user_two_query) }
70
+ let!(:lacking_union_cmd) { User.union.except(user_one_query) }
71
+ end
72
+
73
+ it "should eliminate records that match a given except statement" do
74
+ query = User.union.except(User.select(:id), User.select(:id).where(id: user_one.id))
75
+ expect(query).to match_array([user_two, user_three])
76
+ end
77
+ end
78
+
79
+ describe "union.intersect" do
80
+ it_behaves_like "standard set of errors" do
81
+ let!(:misaligned_cmd) { User.union.intersect(user_one_query, user_two_query) }
82
+ let!(:lacking_union_cmd) { User.union.intersect(user_one_query) }
83
+ end
84
+
85
+ it "should find records with similar attributes" do
86
+ ProfileL.create!(user: user_three, likes: 120)
87
+
88
+ query =
89
+ User.union.intersect(
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")
92
+ )
93
+
94
+ expect(query.pluck(:id)).to have_attributes(size: 1).and(eq([user_one_pl.id]))
95
+ expect(query.first.likes).to eq(user_one_pl.likes)
96
+ end
97
+ end
98
+
99
+ describe "union.as" do
100
+ let(:query) do
101
+ User
102
+ .select("happy_users.id")
103
+ .union(User.where(id: user_one.id), User.where(id: user_three.id))
104
+ .union_as(:happy_users)
105
+ end
106
+
107
+ it "should return two users" do
108
+ expect(query.size).to eq(2)
109
+ end
110
+
111
+ it "should return two userss id's" do
112
+ expect(query.map(&:id)).to match_array([user_one.id, user_three.id])
113
+ end
114
+
115
+ it "should alias the tables being union'd but still allow for accessing table methods" do
116
+ query.each do |happy_person|
117
+ expect(happy_person).to respond_to(:profile_l)
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "union.order_union" do
123
+ it "should order the .union commands" do
124
+ query = User.union(User.where(id: user_one.id), User.where(id: user_three.id)).order_union(id: :desc)
125
+ expect(query).to eq([user_three, user_one])
126
+ end
127
+
128
+ it "should order the .union.all commands" do
129
+ query =
130
+ User.union.all(
131
+ User.where(id: user_one.id),
132
+ User.where(id: user_three.id)
133
+ ).order_union(id: :desc)
134
+
135
+ expect(query).to eq([user_three, user_one])
136
+ end
137
+
138
+ it "should order the union.except commands" do
139
+ query = User.union.except(User.order(id: :asc), User.where(id: user_one.id)).order_union(id: :desc)
140
+ expect(query).to eq([user_three, user_two])
141
+ end
142
+
143
+ it "should order the .union.intersect commands" do
144
+ query =
145
+ User.union.intersect(
146
+ User.where("id < ?", user_three.id),
147
+ User.where("id >= ?", user_one.id)
148
+ ).order_union(id: :desc)
149
+
150
+ expect(query).to eq([user_two, user_one])
151
+ end
152
+ end
153
+
154
+ describe "union.reorder_union" do
155
+ it "should replace the ordering with the new parameters" do
156
+ user_a = User.create!(number: 1)
157
+ user_b = User.create!(number: 10)
158
+ initial_ordering = [user_b, user_a]
159
+ query = User.union(User.where(id: user_a.id), User.where(id: user_b.id)).order_union(id: :desc)
160
+
161
+ expect(query).to eq(initial_ordering)
162
+ expect(query.reorder_union(number: :asc)).to eq(initial_ordering.reverse)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Active Record Window Function Query Methods" do
6
+ let!(:user_one) { User.create! }
7
+ let!(:user_two) { User.create! }
8
+
9
+ let!(:tag_one) { Tag.create!(user: user_one, tag_number: 1) }
10
+ let!(:tag_two) { Tag.create!(user: user_two, tag_number: 2) }
11
+
12
+ let!(:tag_three) { Tag.create!(user: user_one, tag_number: 3) }
13
+ let!(:tag_four) { Tag.create!(user: user_two, tag_number: 4) }
14
+
15
+ let(:tag_group1) { [tag_one, tag_three] }
16
+ let(:tag_group2) { [tag_two, tag_four] }
17
+
18
+ describe ".window_select" do
19
+ context "when using ROW_NUMBER() ordered in asc" do
20
+ let(:base_query) do
21
+ Tag.define_window(:w).partition_by(:user_id, order_by: :tag_number).select(:id)
22
+ end
23
+
24
+ it "should return tag_one with r_id 1 and tag_three with r_id 2" do
25
+ results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
26
+ tag_group1.each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
27
+ end
28
+
29
+ it "should return tag_two with r_id 1 and tag_four with r_id 2" do
30
+ results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
31
+ tag_group2.each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
32
+ end
33
+ end
34
+
35
+ context "when using ROW_NUMBER() ordered in desc" do
36
+ let(:base_query) do
37
+ Tag.define_window(:w).partition_by(:user_id, order_by: { tag_number: :desc }).select(:id)
38
+ end
39
+
40
+ it "should return tag_one with r_id 2 and tag_three with r_id 1" do
41
+ results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
42
+ tag_group1.reverse_each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
43
+ end
44
+
45
+ it "should return tag_two with r_id 2 and tag_four with r_id 1" do
46
+ results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
47
+ tag_group2.reverse_each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Active Record With CTE Query Methods" do
6
+ let!(:user_one) { User.create! }
7
+ let!(:user_two) { User.create! }
8
+ let!(:profile_one) { ProfileL.create!(user_id: user_one.id, likes: 200) }
9
+ let!(:profile_two) { ProfileL.create!(user_id: user_two.id, likes: 500) }
10
+
11
+ describe ".with/1" do
12
+ context "when using as a standalone query" do
13
+ it "should only return a person with less than 300 likes" do
14
+ query = User.with(profile: ProfileL.where("likes < 300"))
15
+ .joins("JOIN profile ON profile.id = users.id")
16
+
17
+ expect(query).to match_array([user_one])
18
+ end
19
+
20
+ it "should return anyone with likes greater than or equal to 200" do
21
+ query = User.with(profile: ProfileL.where("likes >= 200"))
22
+ .joins("JOIN profile ON profile.id = users.id")
23
+
24
+ expect(query).to match_array([user_one, user_two])
25
+ end
26
+ end
27
+
28
+ context "when merging in query" do
29
+ let!(:version_one) { VersionControl.create!(versionable: profile_one, source: { help: "me" }) }
30
+ let!(:version_two) { VersionControl.create!(versionable: profile_two, source: { help: "no one" }) }
31
+
32
+ it "will maintain the CTE table when merging into existing AR queries" do
33
+ sub_query = ProfileL.with(version_controls: VersionControl.where.contains(source: { help: "me" }))
34
+ query = User.joins(profile_l: :version).merge(sub_query)
35
+
36
+ expect(query).to match_array([user_one])
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.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
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+ SimpleCov.start
5
+
6
+ require "active_record_extended"
7
+
8
+ unless ENV["DATABASE_URL"]
9
+ require "dotenv"
10
+ Dotenv.load
11
+ end
12
+
13
+ ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
14
+
15
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require File.expand_path(f) }
16
+ Dir["#{File.dirname(__FILE__)}/**/*examples.rb"].sort.each { |f| require f }
17
+
18
+ RSpec.configure do |config|
19
+ # Enable flags like --only-failures and --next-failure
20
+ config.example_status_persistence_file_path = ".rspec_status"
21
+
22
+ # Disable RSpec exposing methods globally on `Module` and `main`
23
+ config.disable_monkey_patching!
24
+
25
+ config.expect_with :rspec do |c|
26
+ c.syntax = :expect
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Any / None of SQL Queries" do
6
+ let(:equal_query) { '"users"."personal_id" = 1' }
7
+ let(:or_query) { 'OR "users"."personal_id" = 2' }
8
+ let(:equal_or) { "#{equal_query} #{or_query}" }
9
+ let(:join_query) { /INNER JOIN "tags" ON "tags"."user_id" = "users"."id/ }
10
+
11
+ describe "where.any_of/1" do
12
+ it "should group different column arguments into nested or conditions" do
13
+ query = User.where.any_of({ personal_id: 1 }, { id: 2 }, { personal_id: 2 }).to_sql
14
+ expect(query).to match_regex(/WHERE \(\(.+ = 1 OR .+ = 2\) OR .+ = 2\)/)
15
+ end
16
+
17
+ it "Should assign where clause predicates for standard queries" do
18
+ query = User.where.any_of({ personal_id: 1 }, { personal_id: 2 }).to_sql
19
+ expect(query).to include(equal_or)
20
+
21
+ personal_one = User.where(personal_id: 1)
22
+ personal_two = User.where(personal_id: 2)
23
+ query = User.where.any_of(personal_one, personal_two).to_sql
24
+ expect(query).to include(equal_or)
25
+ end
26
+
27
+ it "Joining queries should be added to the select statement" do
28
+ user_two_tag = User.where(personal_id: 1).joins(:hm_tags)
29
+ query = User.where.any_of(user_two_tag).to_sql
30
+ expect(query).to match_regex(join_query)
31
+ expect(query).to include(equal_query)
32
+ end
33
+ end
34
+
35
+ describe "where.none_of/1" do
36
+ it "Should surround the query in a WHERE NOT clause" do
37
+ query = User.where.none_of({ personal_id: 1 }, { id: 2 }, { personal_id: 2 }).to_sql
38
+ expect(query).to match_regex(/WHERE.+NOT \(\(.+ = 1 OR .+ = 2\) OR .+ = 2\)/)
39
+ end
40
+ end
41
+ end