activerecord_any_of 0.0.1 → 1.0.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.
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