activerecord_any_of 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # ActiverecordAnyOf
2
2
 
3
- This method is inspired by [any_of from mongoid](http://two.mongoid.org/docs/querying/criteria.html#any_of).
3
+ This gem provides `#any_of` and `#none_of` on ActiveRecord.
4
+
5
+ `#any_of` is inspired by [any_of from mongoid](http://two.mongoid.org/docs/querying/criteria.html#any_of).
4
6
 
5
7
  It allows to compute an `OR` like query that leverages AR's `#where` syntax:
6
8
 
@@ -24,6 +26,14 @@ Its main purpose is to both :
24
26
  * remove the need to write a sql string when we want an `OR`
25
27
  * allows to write dynamic `OR` queries, which would be a pain with a string
26
28
 
29
+ `#none_of` is the negative version of `#any_of`. This will return all active users :
30
+
31
+ ```ruby
32
+ banned_users = User.where(banned: true)
33
+ unconfirmed_users = User.where("confirmed_at IS NULL")
34
+ active_users = User.none_of(banned_users, unconfirmed_users)
35
+ ```
36
+
27
37
 
28
38
  ## Installation
29
39
 
@@ -0,0 +1,99 @@
1
+ module ActiverecordAnyOf
2
+ class AlternativeBuilder
3
+ def initialize(match_type, context, *queries)
4
+ @builder = match_type == :negative ? NegativeBuilder.new(context, *queries) : PositiveBuilder.new(context, *queries)
5
+ end
6
+
7
+ def build
8
+ @builder.build
9
+ end
10
+
11
+ class Builder
12
+ attr_accessor :queries_bind_values, :queries_joins_values
13
+
14
+ def initialize(context, *source_queries)
15
+ @context, @source_queries = context, source_queries
16
+ @queries_bind_values, @queries_joins_values = [], { includes: [], joins: [], references: [] }
17
+ end
18
+
19
+ def build
20
+ ActiveRecord::Base.connection.supports_statement_cache? ? with_statement_cache : without_statement_cache
21
+ end
22
+
23
+ private
24
+
25
+ def queries
26
+ @queries ||= @source_queries.map do |query|
27
+ query = where(query) if [String, Hash].any? { |type| query.kind_of?(type) }
28
+ query = where(*query) if query.kind_of?(Array)
29
+ self.queries_bind_values += query.bind_values if query.bind_values.any?
30
+ queries_joins_values[:includes] += query.includes_values if query.includes_values.any?
31
+ queries_joins_values[:joins] += query.joins_values if query.joins_values.any?
32
+ queries_joins_values[:references] += query.references_values if Rails.version >= '4' && query.references_values.any?
33
+ query.arel.constraints.reduce(:and)
34
+ end
35
+ end
36
+
37
+ def uniq_queries_joins_values
38
+ @uniq_queries_joins_values ||= queries_joins_values.each { |tables| tables.uniq }
39
+ end
40
+
41
+ def method_missing(method_name, *args, &block)
42
+ @context.send(method_name, *args, &block)
43
+ end
44
+
45
+ def add_joins_to( relation )
46
+ relation = relation.references(uniq_queries_joins_values[:references]) if Rails.version >= '4'
47
+ relation = relation.includes(uniq_queries_joins_values[:includes])
48
+ relation.joins(uniq_queries_joins_values[:joins])
49
+ end
50
+
51
+ def add_related_values_to( relation )
52
+ relation.bind_values += queries_bind_values
53
+ relation.includes_values += uniq_queries_joins_values[:includes]
54
+ relation.joins_values += uniq_queries_joins_values[:joins]
55
+ relation.references_values += uniq_queries_joins_values[:references] if Rails.version >= '4'
56
+
57
+ relation
58
+ end
59
+ end
60
+
61
+ class PositiveBuilder < Builder
62
+ private
63
+
64
+ def with_statement_cache
65
+ relation = where([queries.reduce(:or).to_sql, *queries_bind_values.map { |v| v[1] }])
66
+ add_joins_to relation
67
+ end
68
+
69
+ def without_statement_cache
70
+ relation = where(queries.reduce(:or))
71
+ add_related_values_to relation
72
+ end
73
+ end
74
+
75
+ class NegativeBuilder < Builder
76
+ private
77
+
78
+ def with_statement_cache
79
+ if Rails.version >= '4'
80
+ relation = where.not([queries.reduce(:or).to_sql, *queries_bind_values.map { |v| v[1] }])
81
+ else
82
+ relation = where([Arel::Nodes::Not.new(queries.reduce(:or)).to_sql, *queries_bind_values.map { |v| v[1] }])
83
+ end
84
+
85
+ add_joins_to relation
86
+ end
87
+
88
+ def without_statement_cache
89
+ if Rails.version >= '4'
90
+ relation = where.not(queries.reduce(:or))
91
+ else
92
+ relation = where(Arel::Nodes::Not.new(queries.reduce(:or)))
93
+ end
94
+
95
+ add_related_values_to relation
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiverecordAnyOf
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,3 +1,5 @@
1
+ require 'activerecord_any_of/alternative_builder'
2
+
1
3
  module ActiverecordAnyOf
2
4
  # Returns a new relation, which includes results matching any of conditions
3
5
  # passed as parameters. You can think of it as a sql <tt>OR</tt> implementation.
@@ -21,31 +23,33 @@ module ActiverecordAnyOf
21
23
  # unconfirmed_users = User.where("confirmed_at IS NULL")
22
24
  # unactive_users = User.any_of(banned_users, unconfirmed_users)
23
25
  def any_of(*queries)
24
- queries_bind_values = []
25
- queries = queries.map do |query|
26
- query = where(query) if [String, Hash].any? { |type| query.kind_of?(type) }
27
- query = where(*query) if query.kind_of?(Array)
28
- queries_bind_values += query.bind_values if query.bind_values.any?
29
- query.arel.constraints.reduce(:and)
30
- end
26
+ raise ArgumentError, 'Called any_of() with no arguments.' if queries.none?
27
+ AlternativeBuilder.new(:positive, self, *queries).build
28
+ end
31
29
 
32
- if ActiveRecord::Base.connection.supports_statement_cache?
33
- where([queries.reduce(:or).to_sql, *queries_bind_values.map { |v| v[1] }])
34
- else
35
- relation = where(queries.reduce(:or))
36
- relation.bind_values += queries_bind_values
37
- relation
38
- end
30
+ # Returns a new relation, which includes results not matching any of conditions
31
+ # passed as parameters. It's the negative version of <tt>#any_of</tt>.
32
+ #
33
+ # This will return all active users :
34
+ #
35
+ # banned_users = User.where(banned: true)
36
+ # unconfirmed_users = User.where("confirmed_at IS NULL")
37
+ # active_users = User.none_of(banned_users, unconfirmed_users)
38
+ def none_of(*queries)
39
+ raise ArgumentError, 'Called none_of() with no arguments.' if queries.none?
40
+ AlternativeBuilder.new(:negative, self, *queries).build
39
41
  end
40
42
  end
41
43
 
42
44
  if Rails.version >= '4'
43
45
  module ActiverecordAnyOfDelegation
44
46
  delegate :any_of, to: :all
47
+ delegate :none_of, to: :all
45
48
  end
46
49
  else
47
50
  module ActiverecordAnyOfDelegation
48
51
  delegate :any_of, to: :scoped
52
+ delegate :none_of, to: :scoped
49
53
  end
50
54
  end
51
55
 
@@ -24,4 +24,40 @@ class ActiverecordAnyOfTest < ActiveSupport::TestCase
24
24
  expected = ['Welcome to the weblog', 'So I was thinking']
25
25
  assert_equal expected, david.posts.any_of(welcome, {type: 'SpecialPost'}).map(&:title)
26
26
  end
27
+
28
+ test 'finding alternate dynamically with joined queries' do
29
+ david = Author.where(posts: { title: 'Welcome to the weblog' }).joins(:posts)
30
+ mary = Author.where(posts: { title: "eager loading with OR'd conditions" }).joins(:posts)
31
+
32
+ assert_equal ['David', 'Mary'], Author.any_of(david, mary).map(&:name)
33
+
34
+ if Rails.version >= '4'
35
+ david = Author.where(posts: { title: 'Welcome to the weblog' }).includes(:posts).references(:posts)
36
+ mary = Author.where(posts: { title: "eager loading with OR'd conditions" }).includes(:posts).references(:posts)
37
+ else
38
+ david = Author.where(posts: { title: 'Welcome to the weblog' }).includes(:posts)
39
+ mary = Author.where(posts: { title: "eager loading with OR'd conditions" }).includes(:posts)
40
+ end
41
+
42
+ assert_equal ['David', 'Mary'], Author.any_of(david, mary).map(&:name)
43
+ end
44
+
45
+ test 'finding with alternate negative conditions' do
46
+ assert_equal ['Bob'], Author.none_of({name: 'David'}, {name: 'Mary'}).map(&:name)
47
+ end
48
+
49
+ test 'finding with alternate negative conditions on association' do
50
+ david = Author.where(name: 'David').first
51
+ welcome = david.posts.where(body: 'Such a lovely day')
52
+ expected = ['sti comments', 'sti me', 'habtm sti test']
53
+ assert_equal expected, david.posts.none_of(welcome, {type: 'SpecialPost'}).map(&:title)
54
+ end
55
+
56
+ test 'calling #any_of with no argument raise exception' do
57
+ assert_raise(ArgumentError) { Author.any_of }
58
+ end
59
+
60
+ test 'calling #none_of with no argument raise exception' do
61
+ assert_raise(ArgumentError) { Author.none_of }
62
+ end
27
63
  end
@@ -0,0 +1,2 @@
1
+ class StiPost < Post
2
+ end
Binary file
@@ -88,3 +88,4 @@ Connecting to database specified by database.yml
88
88
   (0.1ms) SELECT version FROM "schema_migrations"
89
89
   (288.7ms) INSERT INTO "schema_migrations" (version) VALUES ('20130617173313')
90
90
   (289.4ms) INSERT INTO "schema_migrations" (version) VALUES ('20130617172335')
91
+ Connecting to database specified by database.yml