active_record_extended 0.7.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -49,7 +49,7 @@ module ActiveRecordExtended
49
49
  when Arel::Nodes::In, Arel::Nodes::Equality
50
50
  column = left_column(arel) || column_from_association(arel)
51
51
 
52
- if %i[hstore jsonb].include?(column.type)
52
+ if [:hstore, :jsonb].include?(column.type)
53
53
  Arel::Nodes::ContainsHStore.new(arel.left, arel.right)
54
54
  elsif column.try(:array)
55
55
  Arel::Nodes::ContainsArray.new(arel.left, arel.right)
@@ -2,16 +2,6 @@
2
2
 
3
3
  module ActiveRecordExtended
4
4
  module QueryMethods
5
- module MergerCTE
6
- def normal_values
7
- super + [:with]
8
- end
9
- end
10
-
11
- module QueryDelegationCTE
12
- delegate :with, to: :all
13
- end
14
-
15
5
  module WithCTE
16
6
  class WithChain
17
7
  def initialize(scope)
@@ -59,12 +49,6 @@ module ActiveRecordExtended
59
49
  self
60
50
  end
61
51
 
62
- def build_arel(*aliases)
63
- super.tap do |arel|
64
- build_with(arel) if with_values?
65
- end
66
- end
67
-
68
52
  def build_with_hashed_value(with_value)
69
53
  with_value.map do |name, expression|
70
54
  select =
@@ -97,5 +81,3 @@ module ActiveRecordExtended
97
81
  end
98
82
 
99
83
  ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::WithCTE)
100
- ActiveRecord::Relation::Merger.prepend(ActiveRecordExtended::QueryMethods::MergerCTE)
101
- ActiveRecord::Querying.prepend(ActiveRecordExtended::QueryMethods::QueryDelegationCTE)
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module Utilities
5
+ A_TO_Z_KEYS = ("a".."z").to_a.freeze
6
+
7
+ # We need to ensure we can flatten nested ActiveRecord::Relations
8
+ # that might have been nested due to the (splat)*args parameters
9
+ #
10
+ # Note: calling `Array.flatten[!]/1` will actually remove all AR relations from the array.
11
+ #
12
+ def flatten_to_sql(*values)
13
+ flatten_safely(values) do |value|
14
+ value = yield value if block_given?
15
+ to_arel_sql(value)
16
+ end
17
+ end
18
+ alias to_sql_array flatten_to_sql
19
+
20
+ def flatten_safely(values, &block)
21
+ unless values.is_a?(Array)
22
+ values = yield values if block_given?
23
+ return [values]
24
+ end
25
+
26
+ values.map { |value| flatten_safely(value, &block) }.reduce(:+)
27
+ end
28
+
29
+ # Applies aliases to the given query
30
+ # Ex: `SELECT * FROM users` => `(SELECT * FROM users) AS "members"`
31
+ def nested_alias_escape(query, alias_name)
32
+ sql_query = Arel::Nodes::Grouping.new(to_arel_sql(query))
33
+ Arel::Nodes::As.new(sql_query, to_arel_sql(double_quote(alias_name)))
34
+ end
35
+
36
+ # Wraps subquery into an Aliased ARRAY
37
+ # Ex: `SELECT * FROM users` => (ARRAY(SELECT * FROM users)) AS "members"
38
+ def wrap_with_array(arel_or_rel_query, alias_name)
39
+ query = Arel::Nodes::NamedFunction.new("ARRAY", to_sql_array(arel_or_rel_query))
40
+ nested_alias_escape(query, alias_name)
41
+ end
42
+
43
+ # Will attempt to digest and resolve the from clause
44
+ #
45
+ # If the from clause is a String, it will check to see if a table reference key has been assigned.
46
+ # - If one cannot be detected, one will be appended.
47
+ # - Rails does not allow assigning table references using the `.from/2` method, when its a string / sym type.
48
+ #
49
+ # If the from clause is an AR relation; it will duplicate the object.
50
+ # - Ensures any memorizers are reset (ex: `.to_sql` sets a memorizer on the instance)
51
+ # - Key's can be assigned using the `.from/2` method.
52
+ #
53
+ def from_clause_constructor(from, reference_key)
54
+ case from
55
+ when /\s.?#{reference_key}.?$/ # The from clause is a string and has the tbl reference key
56
+ @scope.unscoped.from(from)
57
+ when String, Symbol
58
+ @scope.unscoped.from("#{from} #{reference_key}")
59
+ else
60
+ replicate_klass = from.respond_to?(:unscoped) ? from.unscoped : @scope.unscoped
61
+ replicate_klass.from(from.dup, reference_key)
62
+ end
63
+ end
64
+
65
+ # Will carry defined CTE tables from the nested sub-query and gradually pushes it up to the parents query stack
66
+ # I.E: It pushes `WITH [:cte_name:] AS(...), ..` to the top of the query structure tree
67
+ #
68
+ # SPECIAL GOTCHA NOTE: (if duplicate keys are found) This will favor the parents query `with's` over nested ones!
69
+ def pipe_cte_with!(subquery)
70
+ return self unless subquery.try(:with_values?)
71
+
72
+ cte_ary = flatten_safely(subquery.with_values)
73
+ subquery.with_values = nil # Remove nested queries with values
74
+
75
+ # Add subquery's CTE's to the parents query stack. (READ THE SPECIAL NOTE ABOVE!)
76
+ if @scope.with_values?
77
+ # combine top-level and lower level queries `.with` values into 1 structure
78
+ with_hash = cte_ary.each_with_object(@scope.with_values.first) do |from_cte, hash|
79
+ hash.reverse_merge!(from_cte)
80
+ end
81
+
82
+ @scope.with_values = [with_hash]
83
+ else
84
+ # Top level has no with values
85
+ @scope.with!(*cte_ary)
86
+ end
87
+
88
+ self
89
+ end
90
+
91
+ # Ensures the given value is properly double quoted.
92
+ # This also ensures we don't have conflicts with reversed keywords.
93
+ #
94
+ # IE: `user` is a reserved keyword in PG. But `"user"` is allowed and works the same
95
+ # when used as an column/tbl alias.
96
+ def double_quote(value)
97
+ return if value.nil?
98
+
99
+ case value.to_s
100
+ when "*", /^".+"$/ # Ignore keys that contain double quotes or a Arel.star (*)[all columns]
101
+ value
102
+ else
103
+ PG::Connection.quote_ident(value.to_s)
104
+ end
105
+ end
106
+
107
+ # Ensures the key is properly single quoted and treated as a actual PG key reference.
108
+ def literal_key(key)
109
+ case key
110
+ when TrueClass then "'t'"
111
+ when FalseClass then "'f'"
112
+ when Numeric then key
113
+ else
114
+ key = key.to_s
115
+ key.start_with?("'") && key.end_with?("'") ? key : "'#{key}'"
116
+ end
117
+ end
118
+
119
+ # Converts a potential subquery into a compatible Arel SQL node.
120
+ #
121
+ # Note:
122
+ # We convert relations to SQL to maintain compatibility with Rails 5.[0/1].
123
+ # Only Rails 5.2+ maintains bound attributes in Arel, so its better to be safe then sorry.
124
+ # When we drop support for Rails 5.[0/1], we then can then drop the '.to_sql' conversation
125
+
126
+ def to_arel_sql(value)
127
+ case value
128
+ when Arel::Node, Arel::Nodes::SqlLiteral, nil
129
+ value
130
+ when ActiveRecord::Relation
131
+ Arel.sql(value.spawn.to_sql)
132
+ else
133
+ Arel.sql(value.respond_to?(:to_sql) ? value.to_sql : value.to_s)
134
+ end
135
+ end
136
+
137
+ def key_generator
138
+ A_TO_Z_KEYS.sample
139
+ end
140
+ end
141
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "0.7.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -3,17 +3,6 @@
3
3
  require "spec_helper"
4
4
 
5
5
  RSpec.describe "Active Record Inet Query Methods" do
6
- describe "Deprecation Notices" do
7
- %i[contained_within contained_within_or_equals contains_or_equals].each do |method|
8
- it "Should display a deprecation warning for #{method}" do
9
- new_method = "inet_#{method}".to_sym
10
- warning_msg = "##{method} will soon be deprecated for version 1.0 release. Please use ##{new_method} instead."
11
- expect_any_instance_of(ActiveRecordExtended::QueryMethods::Inet).to receive(new_method)
12
- expect { Person.where.send(method, nil) }.to output(Regexp.new(warning_msg)).to_stderr
13
- end
14
- end
15
- end
16
-
17
6
  describe "#inet_contained_within" do
18
7
  let!(:local_1) { Person.create!(ip: "127.0.0.1") }
19
8
  let!(:local_44) { Person.create!(ip: "127.0.0.44") }
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "Active Record JSON methods" do
4
+ let!(:person_one) { Person.create! }
5
+ let!(:person_two) { Person.create! }
6
+
7
+ describe ".select_row_to_json" do
8
+ let!(:tag_one) { Tag.create!(person: person_one, tag_number: 2) }
9
+ let!(:tag_two) { Tag.create!(person: person_two, tag_number: 5) }
10
+ let(:sub_query) { Tag.select(:tag_number).where("tags.person_id = people.id") }
11
+
12
+ it "should nest a json object in the query results" do
13
+ query = Person.select(:id).select_row_to_json(sub_query, as: :results).where(id: person_one.id)
14
+ expect(query.size).to eq(1)
15
+ expect(query.take.results).to be_a(Hash).and(match("tag_number" => 2))
16
+ end
17
+
18
+ # ugh wording here sucks, brain is fried.
19
+ it "accepts a block for appending additional scopes to the middle-top level" do
20
+ query = Person.select(:id).select_row_to_json(sub_query, key: :tag_row, as: :results) do |scope|
21
+ scope.where("tag_row.tag_number = 5")
22
+ end
23
+
24
+ expect(query.size).to eq(2)
25
+ query.each do |result|
26
+ if result.id == person_one.id
27
+ expect(result.results).to be_blank
28
+ else
29
+ expect(result.results).to be_present.and(match("tag_number" => 5))
30
+ end
31
+ end
32
+ end
33
+
34
+ it "allows for casting results in an aggregate-able Array function" do
35
+ query = Person.select(:id).select_row_to_json(sub_query, key: :tag_row, as: :results, cast_as_array: true)
36
+ expect(query.take.results).to be_a(Array).and(be_present)
37
+ expect(query.take.results.first).to be_a(Hash)
38
+ end
39
+
40
+ it "raises an error if a from clause key is missing" do
41
+ expect do
42
+ Person.select(:id).select_row_to_json(key: :tag_row, as: :results)
43
+ end.to raise_error(ArgumentError)
44
+ end
45
+ end
46
+
47
+ describe ".json_build_object" do
48
+ let(:sub_query) do
49
+ Person.select_row_to_json(from: Person.select(:id), cast_as_array: true, as: :ids).where(id: person_one.id)
50
+ end
51
+
52
+ it "defaults the column alias if one is not provided" do
53
+ query = Person.json_build_object(:personal, sub_query)
54
+ expect(query.size).to eq(1)
55
+ expect(query.take.results).to match(
56
+ "personal" => match("ids" => match_array([{ "id" => person_one.id }, { "id" => person_two.id }])),
57
+ )
58
+ end
59
+
60
+ it "allows for re-aliasing the default 'results' column" do
61
+ query = Person.json_build_object(:personal, sub_query, as: :cool_dudes)
62
+ expect(query.take).to respond_to(:cool_dudes)
63
+ end
64
+ end
65
+
66
+ describe ".jsonb_build_object" do
67
+ let(:sub_query) { Person.select(:id, :number).where(id: person_one.id) }
68
+
69
+ it "defaults the column alias if one is not provided" do
70
+ query = Person.jsonb_build_object(:personal, sub_query)
71
+ expect(query.size).to eq(1)
72
+ expect(query.take.results).to be_a(Hash).and(be_present)
73
+ expect(query.take.results).to match("personal" => match("id" => person_one.id, "number" => person_one.number))
74
+ end
75
+
76
+ it "allows for re-aliasing the default 'results' column" do
77
+ query = Person.jsonb_build_object(:personal, sub_query, as: :cool_dudes)
78
+ expect(query.take).to respond_to(:cool_dudes)
79
+ end
80
+
81
+ it "allows for custom value statement" do
82
+ query = Person.jsonb_build_object(
83
+ :personal,
84
+ sub_query.where.not(id: person_one),
85
+ value: "COALESCE(array_agg(\"personal\"), '{}')",
86
+ as: :cool_dudes,
87
+ )
88
+
89
+ expect(query.take.cool_dudes["personal"]).to be_a(Array).and(be_empty)
90
+ end
91
+
92
+ it "will raise a warning if the value doesn't include a double quoted input" do
93
+ expect do
94
+ Person.jsonb_build_object(
95
+ :personal,
96
+ sub_query.where.not(id: person_one),
97
+ value: "COALESCE(array_agg(personal), '{}')",
98
+ as: :cool_dudes,
99
+ )
100
+ end.to output.to_stderr
101
+ end
102
+ end
103
+
104
+ describe "Json literal builds" do
105
+ let(:original_hash) { { p: 1, b: "three", x: 3.14 } }
106
+ let(:hash_as_array_objs) { original_hash.to_a.flatten }
107
+
108
+ shared_examples_for "literal builds" do
109
+ let(:method) { raise "You are expected to over ride this!" }
110
+
111
+ it "will accept a hash arguments that will return itself" do
112
+ query = Person.send(method.to_sym, original_hash)
113
+ expect(query.take.results).to be_a(Hash).and(be_present)
114
+ expect(query.take.results).to match(original_hash.stringify_keys)
115
+ end
116
+
117
+ it "will accept a standard array of key values" do
118
+ query = Person.send(method.to_sym, hash_as_array_objs)
119
+ expect(query.take.results).to be_a(Hash).and(be_present)
120
+ expect(query.take.results).to match(original_hash.stringify_keys)
121
+ end
122
+
123
+ it "will accept a splatted array of key-values" do
124
+ query = Person.send(method.to_sym, *hash_as_array_objs)
125
+ expect(query.take.results).to be_a(Hash).and(be_present)
126
+ expect(query.take.results).to match(original_hash.stringify_keys)
127
+ end
128
+ end
129
+
130
+ describe ".json_build_literal" do
131
+ it_behaves_like "literal builds" do
132
+ let!(:method) { :json_build_literal }
133
+ end
134
+ end
135
+
136
+ describe ".jsonb_build_literal" do
137
+ it_behaves_like "literal builds" do
138
+ let!(:method) { :jsonb_build_literal }
139
+ end
140
+ end
141
+ end
142
+ 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!(:person_one) { Person.create!(number: 8) }
7
+ let!(:person_two) { Person.create!(number: 10) }
8
+ let!(:person_three) { Person.create!(number: 1) }
9
+ let!(:person_one_pl) { ProfileL.create!(person: person_one, likes: 100) }
10
+ let!(:person_two_pl) { ProfileL.create!(person: person_two, likes: 200) }
11
+
12
+ shared_examples_for "standard set of errors" do
13
+ let(:person_one_query) { Person.select(:id).where(id: person_one.id) }
14
+ let(:person_two_query) { Person.select(:id, :tags).where(id: person_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) { Person.union(person_one_query, person_two_query) }
34
+ let!(:lacking_union_cmd) { Person.union(person_one_query) }
35
+ end
36
+
37
+ it "should return two users that match the where conditions" do
38
+ query = Person.union(Person.where(id: person_one.id), Person.where(id: person_three.id))
39
+ expect(query).to match_array([person_one, person_three])
40
+ end
41
+
42
+ it "should allow joins on union statements" do
43
+ query = Person.union(Person.where(id: person_one.id), Person.joins(:profile_l).where.not(id: person_one.id))
44
+ expect(query).to match_array([person_one, person_two])
45
+ end
46
+
47
+ it "should eliminate duplicate results" do
48
+ expected_ids = Person.pluck(:id)
49
+ query = Person.union(Person.select(:id), Person.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) { Person.union.all(person_one_query, person_two_query) }
57
+ let!(:lacking_union_cmd) { Person.union.all(person_one_query) }
58
+ end
59
+
60
+ it "should keep duplicate results from each union statement" do
61
+ expected_ids = Person.pluck(:id) * 2
62
+ query = Person.union.all(Person.select(:id), Person.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) { Person.union.except(person_one_query, person_two_query) }
70
+ let!(:lacking_union_cmd) { Person.union.except(person_one_query) }
71
+ end
72
+
73
+ it "should eliminate records that match a given except statement" do
74
+ query = Person.union.except(Person.select(:id), Person.select(:id).where(id: person_one.id))
75
+ expect(query).to match_array([person_two, person_three])
76
+ end
77
+ end
78
+
79
+ describe "union.intersect" do
80
+ it_behaves_like "standard set of errors" do
81
+ let!(:misaligned_cmd) { Person.union.intersect(person_one_query, person_two_query) }
82
+ let!(:lacking_union_cmd) { Person.union.intersect(person_one_query) }
83
+ end
84
+
85
+ it "should find records with similar attributes" do
86
+ ProfileL.create!(person: person_three, likes: 120)
87
+
88
+ query =
89
+ Person.union.intersect(
90
+ Person.select(:id, "profile_ls.likes").joins(:profile_l).where(profile_ls: { likes: 100 }),
91
+ Person.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([person_one_pl.id]))
95
+ expect(query.first.likes).to eq(person_one_pl.likes)
96
+ end
97
+ end
98
+
99
+ describe "union.as" do
100
+ let(:query) do
101
+ Person.select("happy_people.id")
102
+ .union(Person.where(id: person_one.id), Person.where(id: person_three.id))
103
+ .union_as(:happy_people)
104
+ end
105
+
106
+ it "should return two people" do
107
+ expect(query.size).to eq(2)
108
+ end
109
+
110
+ it "should return two peoples id's" do
111
+ expect(query.map(&:id)).to match_array([person_one.id, person_three.id])
112
+ end
113
+
114
+ it "should alias the tables being union'd but still allow for accessing table methods" do
115
+ query.each do |happy_person|
116
+ expect(happy_person).to respond_to(:profile_l)
117
+ end
118
+ end
119
+ end
120
+
121
+ describe "union.order_union" do
122
+ it "should order the .union commands" do
123
+ query = Person.union(Person.where(id: person_one.id), Person.where(id: person_three.id)).order_union(id: :desc)
124
+ expect(query).to eq([person_three, person_one])
125
+ end
126
+
127
+ it "should order the .union.all commands" do
128
+ query =
129
+ Person.union.all(
130
+ Person.where(id: person_one.id),
131
+ Person.where(id: person_three.id),
132
+ ).order_union(id: :desc)
133
+
134
+ expect(query).to eq([person_three, person_one])
135
+ end
136
+
137
+ it "should order the union.except commands" do
138
+ query = Person.union.except(Person.order(id: :asc), Person.where(id: person_one.id)).order_union(id: :desc)
139
+ expect(query).to eq([person_three, person_two])
140
+ end
141
+
142
+ it "should order the .union.intersect commands" do
143
+ query =
144
+ Person.union.intersect(
145
+ Person.where("id < ?", person_three.id),
146
+ Person.where("id >= ?", person_one.id),
147
+ ).order_union(id: :desc)
148
+
149
+ expect(query).to eq([person_two, person_one])
150
+ end
151
+ end
152
+
153
+ describe "union.reorder_union" do
154
+ it "should replace the ordering with the new parameters" do
155
+ person_a = Person.create!(number: 1)
156
+ person_b = Person.create!(number: 10)
157
+ initial_ordering = [person_b, person_a]
158
+ query = Person.union(Person.where(id: person_a.id), Person.where(id: person_b.id))
159
+ .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