activerecord_finder 0.1.2 → 0.1.4

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
@@ -29,7 +29,8 @@ so you can use ActiveRecord::Base#restrict with a block to construct your querie
29
29
 
30
30
  ## ActiveRecord's Finders
31
31
 
32
- Integrating with ActiveRecord, it is possible to use `restrict` to find records on a ActiveRecord object. The syntax is quite simple:
32
+ Integrating with ActiveRecord, it is possible to use `restrict` to find records on a ActiveRecord object. The syntax is quite simple (Please note that because of operator precedence in Ruby, we must add a parenthesis over the comparission before using `&` or `|`):
33
+
33
34
 
34
35
  ```ruby
35
36
  User.restrict { |f| f.age > 18 }
@@ -73,7 +74,20 @@ User.restrict do
73
74
  end
74
75
  ```
75
76
 
76
- Plase note that because of operator precedence in Ruby, we must add a parenthesis over the comparission before using `&` or `|`.
77
+ Please note that we can't use instance variables if we use the block syntax without a variable. This is a Ruby's limitation, not ActiveRecord Finder's one.
78
+
79
+ ```ruby
80
+ class Foo
81
+ def initialize
82
+ @name = "Bar"
83
+ end
84
+
85
+ def find_all
86
+ User.restrict { name == @name } #Doesn't work - @name is nil
87
+ User.restrict { |u| u.name == @name } #Works!
88
+ end
89
+ end
90
+ ```
77
91
 
78
92
  ## Constructing Queries
79
93
 
@@ -90,6 +104,27 @@ end
90
104
 
91
105
  ### Using "joins"
92
106
 
93
- To be simple, the `Finder` simply has the attributes from the table that it was created. So, to be able to find a record using attributes from one of the joined tables, it is possible to use `ActiveRecord::Base#merge`
107
+ To be simple, the `Finder` simply has the attributes from the table that it was created. So, to be able to find a record using attributes from one of the joined tables, it is possible to use `ActiveRecord::Base#merge`. If it's absolutely necessary, we could use `restrict_with` or `new_finder_with` to construct queries that uses fields from another tables (or table aliases). For example, consider the following examples:
94
108
 
95
- ##
109
+ ```ruby
110
+ #Finding user's roles for specific users
111
+ admins = User.admins #(or User.where(admin: true) or User.restrict { admin == true }
112
+ roles = Role.restrict { name != "top secret administrative stuff" }
113
+ admin_roles = roles.joins(:users).merge(admins)
114
+
115
+ #Finding without "merge" (but you cannot use "scopes" in this case,
116
+ #nor use the block syntax without variables)
117
+ admin_roles = Role.joins(:users).restrict_with(:users) do |role, user|
118
+ (role.name != "top secret administrative stuff") & (user.admin == true)
119
+ end
120
+
121
+ #Finding using a "table alias" or some other thing
122
+ table = User.where(name: "Foo").as('alias')
123
+ User.from([User.arel_table, table]).restrict_with(:alias) do |user, table|
124
+ user.id == table.id
125
+ end
126
+ #Will produce the query:
127
+ #SELECT `users`.*
128
+ #FROM `users`, (SELECT `users`.* FROM `users` WHERE `users`.`name` = 'Foo') alias
129
+ #WHERE `users`.`id` = `alias`.`id`
130
+ ```
@@ -1,5 +1,7 @@
1
1
  require_relative "activerecord_finder/finder"
2
2
  require_relative "activerecord_finder/comparator"
3
3
  require_relative "activerecord_finder/where"
4
+ require_relative "activerecord_finder/set_operations"
4
5
 
5
6
  ActiveRecord::Base.extend ActiveRecordFinder::Where
7
+ ActiveRecord::Relation.send :include, ActiveRecordFinder::SetOperations
@@ -1,5 +1,7 @@
1
1
  module ActiveRecordFinder
2
2
  class Comparator
3
+ attr_reader :field
4
+
3
5
  def initialize(finder, field)
4
6
  @finder = finder
5
7
  @field = field
@@ -7,6 +9,7 @@ module ActiveRecordFinder
7
9
 
8
10
  def self.convert_to_arel(operation, arel_method)
9
11
  define_method(operation) do |other|
12
+ other = other.field if other.respond_to?(:field)
10
13
  arel_clause = @field.send(arel_method, other)
11
14
  Finder.new(@finder.table, arel_clause)
12
15
  end
@@ -22,6 +25,25 @@ module ActiveRecordFinder
22
25
  Finder.new(@finder.table, arel_clause)
23
26
  end
24
27
 
28
+ def not_in?(other)
29
+ other = other.arel if other.respond_to?(:arel)
30
+ arel_clause = @field.not_in(other)
31
+ Finder.new(@finder.table, arel_clause)
32
+ end
33
+
34
+ def size
35
+ Comparator.new(@finder, @field.count)
36
+ end
37
+ alias :count :size
38
+
39
+ def lower
40
+ Comparator.new(@finder, @field.lower)
41
+ end
42
+
43
+ def upper
44
+ Comparator.new(@finder, Arel::Nodes::NamedFunction.new("UPPER", [@field]))
45
+ end
46
+
25
47
  convert_to_arel :==, :eq
26
48
  convert_to_arel '!=', :not_eq
27
49
  convert_to_arel :>, :gt
@@ -5,12 +5,16 @@ module ActiveRecordFinder
5
5
  def initialize(arel_table, operation=nil)
6
6
  @table = arel_table
7
7
  @arel = operation
8
- @table.columns.each { |a| define_attribute_method a }
8
+ @table.columns.each { |a| define_attribute_method a } rescue nil
9
+ end
10
+
11
+ def [](attribute)
12
+ Comparator.new(self, @table[attribute])
9
13
  end
10
14
 
11
15
  def define_attribute_method(arel_attribute)
12
16
  singleton_class.send :define_method, arel_attribute.name do
13
- Comparator.new(self, arel_attribute)
17
+ self[arel_attribute.name]
14
18
  end
15
19
  end
16
20
  private :define_attribute_method
@@ -30,5 +34,10 @@ module ActiveRecordFinder
30
34
  end
31
35
 
32
36
  undef_method :==
37
+
38
+ def method_missing(method, *args, &b)
39
+ super if args.size != 0 || b
40
+ self[method]
41
+ end
33
42
  end
34
43
  end
@@ -0,0 +1,42 @@
1
+ module ActiveRecordFinder
2
+ module SetOperations
3
+ def unite_with(other)
4
+ if arel_table.name != other.arel_table.name
5
+ raise ArgumentError, "can't operate on different tables"
6
+ elsif primary_key.nil?
7
+ raise ArgumentError, "must have a primary key"
8
+ end
9
+
10
+ first_subselect = unscoped.where(primary_key => other)
11
+ second_subselect = unscoped.where(primary_key => self)
12
+ unscoped.restrict(first_subselect.to_finder | second_subselect.to_finder)
13
+ end
14
+
15
+ def intersect_with(other, field_self=nil, field_other=nil)
16
+ field_other ||= field_self
17
+
18
+ if field_self.nil?
19
+ where(primary_key => other)
20
+ else
21
+ if field_other.is_a?(Symbol)
22
+ field_other = other.scoped.arel_table[field_other]
23
+ end
24
+ where(field_self => other.select(field_other))
25
+ end
26
+ end
27
+
28
+ def subtract(other, field_self=nil, field_other=nil)
29
+ field_other ||= field_self
30
+
31
+ if field_self.nil?
32
+ subselect = unscoped.where(primary_key => other)
33
+ else
34
+ if field_other.is_a?(Symbol)
35
+ field_other = other.scoped.arel_table[field_other]
36
+ end
37
+ subselect = unscoped.where(field_self => other.select(field_other))
38
+ end
39
+ restrict(!subselect.to_finder)
40
+ end
41
+ end
42
+ end
@@ -10,22 +10,37 @@ module ActiveRecordFinder
10
10
  end
11
11
  alias :condition :restrict
12
12
 
13
+ def restrict_with(*tables, &block)
14
+ where(create_finder(*tables, &block).arel)
15
+ end
16
+
13
17
  def new_finder(&block)
18
+ new_finder_with(&block)
19
+ end
20
+ alias :to_finder :new_finder
21
+
22
+ def new_finder_with(*tables, &block)
14
23
  scoped_where = scoped.arel.where_clauses.map { |w| Arel.sql(w) }
15
24
  if block
16
- finder = create_finder(&block)
25
+ finder = create_finder(*tables, &block)
17
26
  scoped_where << finder.arel
18
27
  end
19
28
  arel = Arel::Nodes::And.new(scoped_where)
20
29
  ActiveRecordFinder::Finder.new(arel_table, arel)
21
30
  end
31
+ alias :to_finder_with :new_finder_with
22
32
 
23
- def create_finder(&block)
33
+
34
+ def create_finder(*tables, &block)
24
35
  activerecord_finder = ActiveRecordFinder::Finder.new(arel_table)
25
- if block.arity == 1
26
- block.call(activerecord_finder)
27
- else
36
+ if tables.size == 0 && block.arity == 0
28
37
  activerecord_finder.instance_eval(&block)
38
+ else
39
+ additional_finders = tables.map do |table_name|
40
+ arel_table = Arel::Table.new(table_name)
41
+ ActiveRecordFinder::Finder.new(arel_table)
42
+ end
43
+ block.call(activerecord_finder, *additional_finders)
29
44
  end
30
45
  end
31
46
  private :create_finder
@@ -32,6 +32,10 @@ describe ActiveRecordFinder::Comparator do
32
32
  subject.in?(['foo']).arel.should be_equivalent_to table[:name].in(['foo'])
33
33
  end
34
34
 
35
+ it 'finds with NOT IN' do
36
+ subject.not_in?(['foo']).arel.should be_equivalent_to table[:name].not_in(['foo'])
37
+ end
38
+
35
39
  it 'finds with subselects' do
36
40
  result = subject.in?(Person.where(age: 10))
37
41
  result.arel.to_sql.should match(/select/i)
@@ -41,4 +45,14 @@ describe ActiveRecordFinder::Comparator do
41
45
  (subject =~ 'foo').arel.should be_equivalent_to table[:name].matches('foo')
42
46
  (subject !~ 'foo').arel.should be_equivalent_to table[:name].does_not_match('foo')
43
47
  end
48
+
49
+ it 'is able to find with COUNT' do
50
+ (subject.size == 10).arel.should be_equivalent_to table[:name].count.eq(10)
51
+ end
52
+
53
+ it 'finds with LOWER or UPPER' do
54
+ (subject.lower == "foo").arel.should be_equivalent_to table[:name].lower.eq('foo')
55
+ (subject.upper == "foo").arel.should be_equivalent_to(
56
+ Arel::Nodes::NamedFunction.new("UPPER", [table[:name]]).eq('foo'))
57
+ end
44
58
  end
@@ -9,8 +9,8 @@ describe ActiveRecordFinder::Finder do
9
9
  end
10
10
 
11
11
  it 'should not calculate equality' do
12
- proc { subject == subject }.should raise_error(NoMethodError)
13
- proc { subject != subject }.should raise_error(NoMethodError)
12
+ -> { subject == subject }.should raise_error(NoMethodError)
13
+ -> { subject != subject }.should raise_error(NoMethodError)
14
14
  end
15
15
 
16
16
  it 'should be able to "or" two conditions' do
@@ -22,11 +22,16 @@ describe ActiveRecordFinder::Finder do
22
22
  it 'should be able to "and" two conditions' do
23
23
  c1 = table[:id].eq(1)
24
24
  c2 = table[:name].eq(2)
25
- ((subject.id == 1) & (subject.name == 2)).arel.should be_equivalent_to c1.and(c2)
25
+ ((subject[:id] == 1) & (subject[:name] == 2)).arel.should be_equivalent_to c1.and(c2)
26
26
  end
27
27
 
28
28
  it 'should be able to negate the find' do
29
29
  result = subject.name == 'foo'
30
30
  (!result).arel.should be_equivalent_to Arel::Nodes::Not.new(table[:name].eq('foo'))
31
31
  end
32
+
33
+ it 'finds by custom attributes defined in SELECT clause' do
34
+ result = subject.custom == "Foo"
35
+ result.arel.should be_equivalent_to table[:custom].eq("Foo")
36
+ end
32
37
  end
@@ -0,0 +1,54 @@
1
+ require_relative "../helper"
2
+
3
+ describe ActiveRecordFinder::SetOperations do
4
+ let!(:seventeen) { Person.create! :name => 'Foo', :age => 17 }
5
+ let!(:eighteen) { Person.create! :name => 'Foo', :age => 18 }
6
+
7
+ it 'UNIONs two relations' do
8
+ relation1 = Person.where(age: 17)
9
+ relation2 = Person.where(age: 18)
10
+ relation1.unite_with(relation2).should include(seventeen, eighteen)
11
+ end
12
+
13
+ it 'UNIONs two relations with JOIN' do
14
+ seventeen_with_join = Person.create!(age: 17)
15
+ seventeen_with_join.addresses.create!
16
+ relation1 = Person.where(age: 17).joins(:addresses)
17
+ relation2 = Person.where(age: 18)
18
+
19
+ result = relation1.unite_with(relation2)
20
+ result.should include(seventeen_with_join, eighteen)
21
+ result.should_not include(seventeen)
22
+ end
23
+
24
+ it 'INTERSECTs two relations' do
25
+ bar = Person.create!(name: "Bar", age: 17)
26
+ relation1 = Person.where(age: 17)
27
+ relation2 = Person.where(name: "Foo")
28
+ relation1.intersect_with(relation2).should == [seventeen]
29
+ end
30
+
31
+ it 'INTERSECTs two relations using another fields and another tables' do
32
+ seventeen_bar = Person.create!(name: "Bar", age: 17)
33
+ relation1 = Person.where(age: 17)
34
+ relation2 = Person.where(name: "Foo")
35
+ relation1.intersect_with(relation2, :age).should include(seventeen_bar, seventeen)
36
+ relation1.intersect_with(relation2, :age).should_not include(eighteen)
37
+ end
38
+
39
+ it 'SUBTRACTs a relation from this one' do
40
+ relation1 = Person.where(name: "Foo")
41
+ relation2 = Person.where(age: 18)
42
+ relation1.subtract(relation2).should == [seventeen]
43
+ end
44
+
45
+ it 'SUBTRACTs a relation from this one, using another fields' do
46
+ bar = Person.create! name: "Bar", age: 17
47
+ Address.create! address: "Foo"
48
+
49
+ relation1 = Person.where(age: 17)
50
+ relation2 = Address.where(address: "Foo")
51
+
52
+ relation1.subtract(relation2, :name, :address).should == [bar]
53
+ end
54
+ end
@@ -39,4 +39,24 @@ describe ActiveRecordFinder::Where do
39
39
  new_finder = person.new_finder
40
40
  Person.restrict(new_finder).should == [bar]
41
41
  end
42
+
43
+ it 'finds using fields from other tables' do
44
+ seventeen.addresses.create! address: "Seventeen Road"
45
+ eighteen.addresses.create! address: "Eighteen Road"
46
+
47
+ result = Person.joins(:addresses).restrict(
48
+ Person.new_finder_with(:addresses) { |_, a| a.address =~ 'Eighteen%' })
49
+ result.should == [eighteen]
50
+
51
+ result = Person.joins(:addresses).restrict_with(:addresses) { |_, a| a.address =~ 'Seventeen%' }
52
+ result.should == [seventeen]
53
+ end
54
+
55
+ it 'finds by multiple tables or aliases' do
56
+ finder = Person.new_finder_with(:custom) { |p, c| p.id == c.id }
57
+
58
+ people_table = Arel::Table.new(:people)
59
+ custom_table = Arel::Table.new(:custom)
60
+ finder.arel.should be_equivalent_to(people_table[:id].eq(custom_table[:id]))
61
+ end
42
62
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_finder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-03 00:00:00.000000000 Z
12
+ date: 2013-07-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -34,6 +34,7 @@ extensions: []
34
34
  extra_rdoc_files:
35
35
  - README.md
36
36
  files:
37
+ - lib/activerecord_finder/set_operations.rb
37
38
  - lib/activerecord_finder/where.rb
38
39
  - lib/activerecord_finder/finder.rb
39
40
  - lib/activerecord_finder/comparator.rb
@@ -41,6 +42,7 @@ files:
41
42
  - README.md
42
43
  - spec/activerecord_finder/finder_spec.rb
43
44
  - spec/activerecord_finder/comparator_spec.rb
45
+ - spec/activerecord_finder/set_operations_spec.rb
44
46
  - spec/activerecord_finder/where_spec.rb
45
47
  homepage: http://github.com/mauricioszabo/arel_operators
46
48
  licenses: []
@@ -69,5 +71,6 @@ summary: Better finder syntax (|, &, >=, <=) for ActiveRecord.
69
71
  test_files:
70
72
  - spec/activerecord_finder/finder_spec.rb
71
73
  - spec/activerecord_finder/comparator_spec.rb
74
+ - spec/activerecord_finder/set_operations_spec.rb
72
75
  - spec/activerecord_finder/where_spec.rb
73
76
  has_rdoc: true