active_record_extended 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd022475e7df8677d1211cfefe42f7090eca4a29fe54b96254f9490baaf101f0
4
- data.tar.gz: ae8827a88842bcba66d440ed1ffa540349007e124516ad2d2d9e8f64d553f840
3
+ metadata.gz: 6d852a8ee92e11e7a68b77e9763553d43e965778fdebd063a7d9bce6f94e9094
4
+ data.tar.gz: 72b31962b0f85f57436e2f364022a1f43edae3d471eff1efcb1b8b7d066b05d1
5
5
  SHA512:
6
- metadata.gz: 5ff916a3e069accb63c3d98c1f46705ac2191de4c40a7b8f67415e0f752c420f6efd5c6601041b11bcc6cf7ae0bd776a4a2c22d2b89c2239bb9fb68c04ba40ed
7
- data.tar.gz: 41b2f1e01aab68f69bc6753066c242475a3ecdd6f8fed68214065c5a55ecef5b9c8d261c6bf3f6d9264f9031775ef5d8571f43ca48561a36432c273672d11c9a
6
+ metadata.gz: 1170988a8f118aaa08a7257177427748c584aebe340310aba9afe93ec4878dc237efd8098c071029ef5df200d30f1349b02f6f0e1a3862b8bc5d14f20d0c00c9
7
+ data.tar.gz: cc6b9102753928f300986767cf4c9e7466ae8758e55cabc54e0ef967a40c95c981199f4319bde271155868f02bc6fec4b77f3889585354ba56023ca3bf7c271d
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # 0.2.0 - May 6th 2018
2
+
3
+ Added ActiveRecord Where Chain Functionality
4
+ - .where.any_of
5
+ - .where.none_of
6
+
7
+ # 0.1.1 - May 2nd 2018
8
+
9
+ Added ActiveRecord Where Chain Functionality:
10
+ - .where.overlap
11
+ - .where.contained_within
12
+ - .where.contained_within_or_equals
13
+ - .where.contains_or_equals
14
+ - .where.any/1
15
+ - .where.all/1
16
+
17
+ Added ActiveRecord Base Extentions
18
+ - .either_order/2
19
+ - .either_join/2
20
+
data/Gemfile.lock CHANGED
@@ -29,7 +29,7 @@ GEM
29
29
  concurrent-ruby (1.0.5)
30
30
  database_cleaner (1.7.0)
31
31
  diff-lcs (1.3)
32
- dotenv (2.2.2)
32
+ dotenv (2.4.0)
33
33
  i18n (1.0.1)
34
34
  concurrent-ruby (~> 1.0)
35
35
  method_source (0.9.0)
@@ -8,8 +8,8 @@ require "active_record_extended/version"
8
8
  Gem::Specification.new do |spec|
9
9
  spec.name = "active_record_extended"
10
10
  spec.version = ActiveRecordExtended::VERSION
11
- spec.authors = ["George Protacio-Karaszi", "Dan McClain"]
12
- spec.email = ["georgekaraszi@gmail.com", "git@danmcclain.net"]
11
+ spec.authors = ["George Protacio-Karaszi", "Dan McClain", "Olivier El Mekki"]
12
+ spec.email = ["georgekaraszi@gmail.com", "git@danmcclain.net", "olivier@el-mekki.com"]
13
13
 
14
14
  spec.summary = "Adds extended functionality to Activerecord Postgres implementation"
15
15
  spec.description = "Adds extended functionality to Activerecord Postgres implementation"
@@ -5,6 +5,7 @@ require "active_record"
5
5
  require "active_record_extended/predicate_builder/array_handler_decorator"
6
6
  require "active_record_extended/query_methods/where_chain"
7
7
  require "active_record_extended/query_methods/either"
8
+ require "active_record_extended/query_methods/any_of"
8
9
 
9
10
  if ActiveRecord::VERSION::MAJOR >= 5
10
11
  if ActiveRecord::VERSION::MINOR >= 2
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module QueryMethods
5
+ module AnyOf
6
+ def any_of(*queries)
7
+ queries = hash_map_queries(queries)
8
+ build_query(queries) do |arel_query, binds|
9
+ if binds.any?
10
+ @scope.where(unprepared_query(arel_query.to_sql), *binds)
11
+ else
12
+ @scope.where(arel_query)
13
+ end
14
+ end
15
+ end
16
+
17
+ def none_of(*queries)
18
+ queries = hash_map_queries(queries)
19
+ build_query(queries) do |arel_query, binds|
20
+ if binds.any?
21
+ @scope.where.not(unprepared_query(arel_query.to_sql), *binds)
22
+ else
23
+ @scope.where.not(arel_query)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def hash_map_queries(queries)
31
+ if queries.count == 1 && queries.first.is_a?(Hash)
32
+ queries.first.each_pair.map { |attr, predicate| Hash[attr, predicate] }
33
+ else
34
+ queries
35
+ end
36
+ end
37
+
38
+ def build_query(queries)
39
+ query_map = construct_query_mappings(queries)
40
+ query = yield(query_map[:arel_query], query_map[:binds])
41
+ query.joins(query_map[:joins].to_a)
42
+ .includes(query_map[:includes].to_a)
43
+ .references(query_map[:references].to_a)
44
+ end
45
+
46
+ def construct_query_mappings(queries) # rubocop:disable Metrics/AbcSize
47
+ { joins: Set.new, references: Set.new, includes: Set.new, arel_query: nil, binds: [] }.tap do |query_map|
48
+ query_map[:arel_query] = queries.map do |raw_query|
49
+ query = generate_where_clause(raw_query)
50
+ query_map[:joins] << translate_reference(query.joins_values) if query.joins_values.any?
51
+ query_map[:includes] << translate_reference(query.includes_values) if query.includes_values.any?
52
+ query_map[:references] << translate_reference(query.references_values) if query.references_values.any?
53
+ query_map[:binds] += bind_attributes(query)
54
+ query.arel.constraints.reduce(:and)
55
+ end.reduce(:group_or)
56
+ end
57
+ end
58
+
59
+ # Rails 5.1 fix
60
+ # In Rails 5.2 the arel table maintains attribute binds
61
+ def bind_attributes(query)
62
+ return [] unless query.respond_to?(:bound_attributes)
63
+ query.bound_attributes.map(&:value)
64
+ end
65
+
66
+ # Rails 5.1 fix
67
+ def unprepared_query(query)
68
+ query.gsub(/((?<!\\)'.*?(?<!\\)'|(?<!\\)".*?(?<!\\)")|(\=\ \$\d+)/) do |match|
69
+ Regexp.last_match(2)&.gsub(/\=\ \$\d+/, "= ?") || match
70
+ end
71
+ end
72
+
73
+ def translate_reference(reference)
74
+ reference.map { |ref| ref.try(:to_sql) || ref }.compact
75
+ end
76
+
77
+ def generate_where_clause(query)
78
+ case query
79
+ when String, Hash
80
+ @scope.where(query)
81
+ when Array
82
+ @scope.where(*query)
83
+ else
84
+ query
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ ActiveRecord::QueryMethods::WhereChain.prepend(ActiveRecordExtended::QueryMethods::AnyOf)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "Active Record Any / None of Methods" do
4
+ let!(:one) { Person.create!(personal_id: 1) }
5
+ let!(:two) { Person.create!(personal_id: 2) }
6
+ let!(:three) { Person.create!(personal_id: 3) }
7
+
8
+ let!(:tag_one) { Tag.create!(person_id: one.id) }
9
+ let!(:tag_two) { Tag.create!(person_id: two.id) }
10
+ let!(:tag_three) { Tag.create!(person_id: three.id) }
11
+
12
+ describe "where.any_of/1" do
13
+ it "Should return queries that match any of the outlined queries" do
14
+ query = Person.where.any_of({ personal_id: 1 }, { personal_id: 2 })
15
+ expect(query).to include(one, two)
16
+ expect(query).to_not include(three)
17
+ end
18
+
19
+ it "Should accept where query predicates" do
20
+ personal_one = Person.where(personal_id: 1)
21
+ personal_two = Person.where(personal_id: 2)
22
+ query = Person.where.any_of(personal_one, personal_two)
23
+
24
+ expect(query).to include(one, two)
25
+ expect(query).to_not include(three)
26
+ end
27
+
28
+ it "Should accept query strings" do
29
+ personal_one = Person.where(personal_id: 1)
30
+
31
+ query = Person.where.any_of(personal_one, "personal_id > 2")
32
+ expect(query).to include(one, three)
33
+ expect(query).to_not include(two)
34
+
35
+ query = Person.where.any_of(["personal_id >= ?", 2])
36
+ expect(query).to include(two, three)
37
+ expect(query).to_not include(one)
38
+ end
39
+
40
+ context "Relationship queries" do
41
+ it "Finds records that are queried from two or more has_many associations" do
42
+ person_one_tag = Tag.create!(person_id: one.id)
43
+ person_two_tag = Tag.create!(person_id: two.id)
44
+ query = Tag.where.any_of(one.hm_tags, two.hm_tags)
45
+
46
+ expect(query).to include(tag_one, tag_two, person_one_tag, person_two_tag)
47
+ expect(query).to_not include(tag_three)
48
+ end
49
+
50
+ it "Finds records that are dynamically joined" do
51
+ person_one_tag = Tag.where(people: { id: one.id }).includes(:person).references(:person)
52
+ person_two_tag = Tag.where(people: { id: two.id }).joins(:person)
53
+ query = Tag.where.any_of(person_one_tag, person_two_tag)
54
+
55
+ expect(query).to include(tag_one, tag_two)
56
+ expect(query).to_not include(tag_three)
57
+ end
58
+
59
+ it "Return matched records of a joined table on the parent level" do
60
+ query = Tag.joins(:person).where.any_of(
61
+ { people: { personal_id: 1 } },
62
+ { people: { personal_id: 3 } },
63
+ )
64
+
65
+ expect(query).to include(tag_one, tag_three)
66
+ expect(query).to_not include(tag_two)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "where.none_of/1" do
72
+ it "Should return queries that match none of the outlined queries" do
73
+ query = Person.where.none_of({ personal_id: 1 }, { personal_id: 2 })
74
+ expect(query).to include(three)
75
+ expect(query).to_not include(one, two)
76
+ end
77
+
78
+ it "Should accept where query predicates" do
79
+ personal_one = Person.where(personal_id: 1)
80
+ personal_two = Person.where(personal_id: 2)
81
+ query = Person.where.none_of(personal_one, personal_two)
82
+
83
+ expect(query).to include(three)
84
+ expect(query).to_not include(one, two)
85
+ end
86
+
87
+ it "Should accept query strings" do
88
+ personal_one = Person.where(personal_id: 1)
89
+
90
+ query = Person.where.none_of(personal_one, "personal_id > 2")
91
+ expect(query).to include(two)
92
+ expect(query).to_not include(one, three)
93
+
94
+ query = Person.where.none_of(["personal_id >= ?", 2])
95
+ expect(query).to include(one)
96
+ expect(query).to_not include(two, three)
97
+ end
98
+
99
+ context "Relationship queries" do
100
+ it "Finds records that are queried from two or more has_many associations" do
101
+ person_one_tag = Tag.create!(person_id: one.id)
102
+ person_two_tag = Tag.create!(person_id: two.id)
103
+ query = Tag.where.none_of(one.hm_tags, two.hm_tags)
104
+
105
+ expect(query).to include(tag_three)
106
+ expect(query).to_not include(tag_one, tag_two, person_one_tag, person_two_tag)
107
+ end
108
+
109
+ it "Finds records that are dynamically joined" do
110
+ person_one_tag = Tag.where(people: { id: one.id }).includes(:person).references(:person)
111
+ person_two_tag = Tag.where(people: { id: two.id }).joins(:person)
112
+ query = Tag.where.none_of(person_one_tag, person_two_tag)
113
+
114
+ expect(query).to include(tag_three)
115
+ expect(query).to_not include(tag_one, tag_two)
116
+ end
117
+
118
+ it "Return matched records of a joined table on the parent level" do
119
+ query = Tag.joins(:person).where.none_of(
120
+ { people: { personal_id: 1 } },
121
+ { people: { personal_id: 3 } },
122
+ )
123
+
124
+ expect(query).to include(tag_two)
125
+ expect(query).to_not include(tag_one, tag_three)
126
+ end
127
+ end
128
+ end
129
+ 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) { "people\".\"personal_id\" = 1" }
7
+ let(:or_query) { "OR (\"people\".\"personal_id\" = 2)" }
8
+ let(:equal_or) { equal_query + " " + or_query }
9
+ let(:join_query) { /INNER JOIN \"tags\" ON \"tags\".\"person_id\" = \"people\".\"id/ }
10
+
11
+ describe "where.any_of/1" do
12
+ it "should group different column arguments into nested or conditions" do
13
+ query = Person.where.any_of({ personal_id: 1 }, { id: 2 }, { personal_id: 2 }).to_sql
14
+ expect(query).to match_regex(/WHERE .+ = 1 OR \(.+\) OR \(.+\)/)
15
+ end
16
+
17
+ it "Should assign where clause predicates for standard queries" do
18
+ query = Person.where.any_of({ personal_id: 1 }, { personal_id: 2 }).to_sql
19
+ expect(query).to include(equal_or)
20
+
21
+ personal_one = Person.where(personal_id: 1)
22
+ personal_two = Person.where(personal_id: 2)
23
+ query = Person.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
+ person_two_tag = Person.where(personal_id: 1).joins(:hm_tags)
29
+ query = Person.where.any_of(person_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 = Person.where.none_of({ personal_id: 1 }, { id: 2 }, { personal_id: 2 }).to_sql
38
+ expect(query).to match_regex(/WHERE.+NOT \(.+ = 1 OR \(.+\) OR \(.+\)\)/)
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_extended
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Protacio-Karaszi
8
8
  - Dan McClain
9
+ - Olivier El Mekki
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2018-05-05 00:00:00.000000000 Z
13
+ date: 2018-05-06 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: activerecord
@@ -119,6 +120,7 @@ description: Adds extended functionality to Activerecord Postgres implementation
119
120
  email:
120
121
  - georgekaraszi@gmail.com
121
122
  - git@danmcclain.net
123
+ - olivier@el-mekki.com
122
124
  executables:
123
125
  - console
124
126
  - setup
@@ -132,6 +134,7 @@ files:
132
134
  - ".ruby-gemset"
133
135
  - ".ruby-version"
134
136
  - ".travis.yml"
137
+ - CHANGELOG.md
135
138
  - CODE_OF_CONDUCT.md
136
139
  - Gemfile
137
140
  - Gemfile.lock
@@ -153,14 +156,17 @@ files:
153
156
  - lib/active_record_extended/patch/5_1/where_clause.rb
154
157
  - lib/active_record_extended/patch/5_2/where_clause.rb
155
158
  - lib/active_record_extended/predicate_builder/array_handler_decorator.rb
159
+ - lib/active_record_extended/query_methods/any_of.rb
156
160
  - lib/active_record_extended/query_methods/either.rb
157
161
  - lib/active_record_extended/query_methods/where_chain.rb
158
162
  - lib/active_record_extended/version.rb
159
163
  - spec/active_record_extended_spec.rb
164
+ - spec/query_methods/any_of_spec.rb
160
165
  - spec/query_methods/array_query_spec.rb
161
166
  - spec/query_methods/either_spec.rb
162
167
  - spec/query_methods/hash_query_spec.rb
163
168
  - spec/spec_helper.rb
169
+ - spec/sql_inspections/any_of_sql_spec.rb
164
170
  - spec/sql_inspections/arel/array_spec.rb
165
171
  - spec/sql_inspections/contains_sql_queries_spec.rb
166
172
  - spec/sql_inspections/either_sql_spec.rb
@@ -192,10 +198,12 @@ specification_version: 4
192
198
  summary: Adds extended functionality to Activerecord Postgres implementation
193
199
  test_files:
194
200
  - spec/active_record_extended_spec.rb
201
+ - spec/query_methods/any_of_spec.rb
195
202
  - spec/query_methods/array_query_spec.rb
196
203
  - spec/query_methods/either_spec.rb
197
204
  - spec/query_methods/hash_query_spec.rb
198
205
  - spec/spec_helper.rb
206
+ - spec/sql_inspections/any_of_sql_spec.rb
199
207
  - spec/sql_inspections/arel/array_spec.rb
200
208
  - spec/sql_inspections/contains_sql_queries_spec.rb
201
209
  - spec/sql_inspections/either_sql_spec.rb