arel_operators 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,12 +1,10 @@
1
- AR Operators
1
+ = AR Operators =
2
2
 
3
3
  Don't let me wrong, it's not as I hate SQL. I just hate to create complex queries when ActiveRecord
4
4
  could do this for me, for free.
5
5
 
6
- Enter AR Operators.
7
-
8
- Imagine you're finding people. You want everybody named "John" OR "Smith", BUT don't want anyone who is underaged.
9
- So, in ActiveRecord, you could do something like:
6
+ For example, imagine you're finding people. You want everybody named "John" OR "Smith",
7
+ BUT don't want anyone who is underaged. So, in ActiveRecord, you could do something like:
10
8
 
11
9
  Person.all :conditions => [
12
10
  '(name LIKE ? OR name LIKE ?) AND age >= 18', '%John%', '%Smith%'
@@ -20,38 +18,92 @@ Better. But why not:
20
18
 
21
19
  johns = Person.where(['name like ?', '%John%'])
22
20
  smiths = Person.where(['name like ?', '%Smith%'])
23
- underageds = Person.where('age < 18')
24
- (johns | smiths) - underageds
21
+ underaged = Person.where('age < 18')
22
+ (johns | smiths) - underaged
25
23
 
26
- ENTER AR Operators
24
+ == ENTER AR Operators ==
27
25
 
28
26
  This library brings operators to ActiveRecord 3. So, all you have to do is:
29
27
  require 'ar_operators'
30
28
  class Person < ActiveRecord::Base
31
- extend AROperators
29
+ extend ArelOperators
32
30
  end
33
31
 
34
32
  And you're ready to go. Right now, the following operators are implemented:
35
- | (OR)
36
- & (AND)
33
+ .or, |, + (OR)
34
+ .and (AND)
37
35
  - (AND (NOT ...))
38
36
  -@ (negates the query. Use like: -Person.where(:name => 'foo'), to find all people where name is not 'foo')
39
37
 
38
+ I cannot use & (amperstand operator) for AND, because this behaviour is already defined for ActiveRecord::Relation.
39
+
40
40
  There is also the following constructions:
41
41
  p1 = Person.where :name => 'Foo'
42
42
  p2 = Person.where :age => 18
43
43
  p1.where(p2) #Generates something like: SELECT * FROM people WHERE ((name = 'Foo') AND (id in SELECT id FROM people WHERE age = 18))
44
44
 
45
- Design Decision:
45
+ == Design Decision: ==
46
46
  To not monkey patch ActiveRecord::Relation, I decided to include these operators only when you
47
47
  use "where" or "scoped" to find objects. If you decide that ALL ActiveRecord operations should
48
48
  have this kind of behaviour, you can:
49
49
 
50
50
  class ActiveRecord::Relation
51
- include ActiveRecord::Operators
51
+ include ArelOperators::Operators
52
52
  end
53
53
 
54
- Known issues:
54
+ == Finders ==
55
+ Ok, so we don't have to write tedious SQL operations by hand. What about trying a little further,
56
+ and not write ANY SQL at all? After all, we have blocks in Ruby, so what about:
57
+
58
+ Person.where { |p| p.name >= 10 }
59
+
60
+ ArelOperators, right now, permits this kind of behaviour only for where clauses (no "having" support
61
+ right now). The following operations are supported:
62
+ == (Test for equality)
63
+ != (Test for inequality, Ruby 1.9 only)
64
+ >, >= (Test for Greater or Greather or Equal)
65
+ <, <= (Test for Lower, or Lower or equal)
66
+ in? (SQL IN clause, "WHERE name IN(1, 2, 3)" )
67
+ like?, matches?, =~ (SQL LIKE clause, "WHERE name LIKE 'something'")
68
+
69
+ So, as an example:
70
+
71
+ Person.where { |p| p.name.like?("F%") }
72
+ Person.where { |p| p.name =~ "F%" } #Same as above
73
+ Person.where { |p| p.age.in?([10, 20, 30]) } #SQL IN operator
74
+ Person.where { |p| p.age.in?(0..17) } #SQL BETWEEN operator
75
+
76
+ Furthermore, you can OR or AND your clauses with | and &. Ruby 1.9 "!" and is also supported, and for
77
+ Ruby 1.8, you can use the negative ("-@") operator. For example:
78
+
79
+ Person.where { |p| (p.name != "Fred") | (p.age > 17) } #Ruby 1.9 version
80
+ Person.where { |p| !(p.name == "Fred") | (p.age > 17) } #Another Ruby 1.9 version
81
+ Person.where { |p| -(p.name == "Fred") | (p.age > 17) } #All ruby versions
82
+
83
+ The block argument is optional, too, so:
84
+
85
+ Person.where { name == "Foo" }
86
+ Person.where { name.like "F%" }
87
+ Person.where { (age <= 10) & (name =~ "F%) }
88
+
89
+ Please notice, that as & and | operators have a high precedence, don't forget the parenthesis,
90
+ otherwise undesired behaviours can occur.
91
+
92
+ This kind of syntax can be combined with regular "where" expressions too. So, the following code:
93
+
94
+ Person.where(:name => "Foo") { age > 10 }
95
+
96
+ Is the same as:
97
+
98
+ Person.where(:name => "Foo").where { age > 10 }
99
+
100
+ (this will "AND" the two conditions)
101
+
102
+ == NOTICE ==
103
+ Regexp operations are NOT supported. Right now, I'm only delegating these operators
104
+ to Arel, and Arel still has no support for regexp.
105
+
106
+ == Known issues: ==
55
107
  As ActiveRecord::Relation doesn't only include "where" clauses, there can be a strange behaviour if trying
56
108
  to combinate more behaviours. For instance, this kind of query:
57
109
  p1 = Person.where(:name => 'Foo').limit(10)
@@ -63,3 +115,10 @@ SELECT "people".* FROM "people" WHERE ((("people"."name" = 'Foo') OR ("people".
63
115
 
64
116
  So, if you don't want to fall in undefined behaviours, please use:
65
117
  (p1 | p2).limit(20).order('name')
118
+
119
+ Furthermore, beware of "OR" operator. Don't try this at home:
120
+ p1 = Person.where(:name => "Foo")
121
+ a1 = Address.where(:road => "Something")
122
+ (p1 | a1) #This will generate something like:
123
+ SELECT "people".* FROM "people" WHERE ((("people"."name" = 'Foo') OR ("addresses"."road" = 'Something')))
124
+ So, as with & operator, you must join relations so your query would be correct.
@@ -1,16 +1,32 @@
1
- require "active_record/operators"
1
+ require "arel_operators/operators"
2
+ require 'arel_operators/finder'
2
3
  module ArelOperators
3
- def where(args, *opts)
4
+ def where(*args, &b)
5
+ if b
6
+ arel = arel_from_block &b
7
+ result = where(arel)
8
+ return result if args.empty?
9
+ return result.where(*args)
10
+ end
4
11
  include_operators_on super
5
12
  end
6
13
 
14
+ def arel_from_block(&b)
15
+ if b.arity == -1 || b.arity == 0
16
+ ArelOperators::Finder.new(arel_table).instance_eval(&b).arel
17
+ else
18
+ b.call(ArelOperators::Finder.new(arel_table)).arel
19
+ end
20
+ end
21
+ private :arel_from_block
22
+
7
23
  def scoped(*args)
8
24
  include_operators_on super(*args)
9
25
  end
10
26
 
11
27
  def include_operators_on(relation)
12
28
  metaclass = class << relation; self; end
13
- metaclass.send :include, ActiveRecord::Operators
29
+ metaclass.send :include, ArelOperators::Operators
14
30
  return relation
15
31
  end
16
32
  private :include_operators_on
@@ -0,0 +1,45 @@
1
+ require "arel_operators/finder/comparators"
2
+ module ArelOperators
3
+ class Finder
4
+ attr_reader :table, :arel
5
+
6
+ def initialize(arel_table, operation=nil)
7
+ @table = arel_table
8
+ @arel = operation
9
+ @table.attributes.each { |a| define_attribute_method a.name }
10
+ end
11
+
12
+ def define_attribute_method(attribute)
13
+ singleton_class.send :define_method, attribute do
14
+ ArelOperators::Finder::Comparators.new(self, @table[attribute])
15
+ end
16
+ end
17
+ private :define_attribute_method
18
+
19
+ def |(other)
20
+ other = other.arel if other.respond_to?(:arel)
21
+ Finder.new(table, arel.or(other))
22
+ end
23
+
24
+ def &(other)
25
+ other = other.arel if other.respond_to?(:arel)
26
+ Finder.new(table, arel.and(other))
27
+ end
28
+
29
+ if RUBY_VERSION >= '1.9.0'
30
+ eval '
31
+ def !@
32
+ -self
33
+ end
34
+ '
35
+ end
36
+
37
+ def -@
38
+ Finder.new(table, Arel::Predicates::Not.new(arel))
39
+ end
40
+
41
+ undef_method :==
42
+ end
43
+ end
44
+
45
+ __END__
@@ -0,0 +1,32 @@
1
+ module ArelOperators
2
+ class Finder
3
+ class Comparators
4
+ def initialize(finder, field)
5
+ @finder = finder
6
+ @field = finder.table[field]
7
+ end
8
+
9
+ def self.convert_to_arel(operation, arel_method)
10
+ define_method(operation) do |other|
11
+ arel_clause = @field.send(arel_method, other)
12
+ ArelOperators::Finder.new(@finder.table, arel_clause)
13
+ end
14
+ end
15
+
16
+ def nil?
17
+ self == nil
18
+ end
19
+
20
+ convert_to_arel :==, :eq
21
+ convert_to_arel '!=', :not_eq
22
+ convert_to_arel :>, :gt
23
+ convert_to_arel :>=, :gteq
24
+ convert_to_arel :<, :lt
25
+ convert_to_arel :<=, :lteq
26
+ convert_to_arel :in?, :in
27
+ convert_to_arel :like?, :matches
28
+ alias :matches? :like?
29
+ alias :=~ :like? #TODO: Is this really a good idea?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ module ArelOperators
2
+ module Operators
3
+ def or(other)
4
+ build_arel_predicate(Arel::Predicates::Or, self, other)
5
+ end
6
+ alias :| :or
7
+ alias :+ :or
8
+
9
+ def and(other)
10
+ build_arel_predicate(Arel::Predicates::And, self, other)
11
+ end
12
+
13
+ def -(other)
14
+ self.and(-other)
15
+ end
16
+
17
+ def -@
18
+ build_arel_predicate(Arel::Predicates::Not, self)
19
+ end
20
+
21
+ def where(obj, *args)
22
+ return super unless obj.respond_to?(:build_where)
23
+ relation = obj.select(:id)
24
+ other = clone.tap do |c|
25
+ cond = build_where(Arel::Predicates::In.new(relation.primary_key, relation.arel))
26
+ c.where_values = Array.wrap(cond)
27
+ end
28
+ self.and(other)
29
+ end
30
+
31
+ private
32
+ def build_arel_predicate(predicate, *args)
33
+ clone.tap do |c|
34
+ condition = build_predicate(predicate, *args)
35
+ cond = build_where(condition)
36
+ c.where_values = Array.wrap(cond)
37
+ possible_joins = args.select { |x| x.object_id != object_id && x.respond_to?(:joins_values) }
38
+ c.joins_values += possible_joins.collect { |x| x.joins_values }
39
+ #possible_eager = args.select { |x| x.object_id != object_id }
40
+ #c.eager_load_values += possible_eager.collect { |x| x.eager_load_values }.flatten
41
+ end
42
+ end
43
+
44
+ def build_predicate(predicate, *relations)
45
+ values = relations.collect do |relation|
46
+ value = relation.is_a?(ActiveRecord::Relation) ? relation.where_values : relation
47
+ wrap_predicate_value(value)
48
+ end
49
+ predicate.new(*values)
50
+ end
51
+
52
+ def wrap_predicate_value(value)
53
+ value.collect do |v|
54
+ next v unless v.is_a?(String)
55
+ Arel::SqlLiteral.new(v)
56
+ end
57
+ end
58
+ end
59
+
60
+ class RelationMismatch < StandardError
61
+ def initialize(expected_class)
62
+ super("Invalid class for operation. Expected #{expected_class}")
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ require "spec/helper"
2
+
3
+ describe ArelOperators::Finder::Comparators do
4
+ before do
5
+ @table = Person.arel_table
6
+ @finder = ArelOperators::Finder.new Person.arel_table
7
+ @comparator = ArelOperators::Finder::Comparators.new @finder, :name
8
+ end
9
+
10
+ it 'should be able to find by equality' do
11
+ (@comparator == 'foo').should be_a_kind_of(ArelOperators::Finder)
12
+ (@comparator == 'foo').arel.should ==
13
+ @table[:name].eq('foo')
14
+ end
15
+
16
+ it 'should be able to find by inequality (Ruby1.9 only)' do
17
+ next if RUBY_VERSION < '1.9.0'
18
+ (@comparator != 'foo').arel.should ==
19
+ @table[:name].not_eq('foo')
20
+ end
21
+
22
+ it 'should be able to verify if an attribute is nil' do
23
+ (@comparator.nil?).arel.should ==
24
+ @table[:name].eq(nil)
25
+ end
26
+
27
+ it 'should be able to find by greater than' do
28
+ (@comparator > 'foo').arel.should == @table[:name].gt('foo')
29
+ (@comparator >= 'foo').arel.should == @table[:name].gteq('foo')
30
+ end
31
+
32
+ it 'should be able to find by lower than' do
33
+ (@comparator < 'foo').arel.should == @table[:name].lt('foo')
34
+ (@comparator <= 'foo').arel.should == @table[:name].lteq('foo')
35
+ end
36
+
37
+ it 'should be able to find IN' do
38
+ @comparator.in?(['foo']).arel.should == @table[:name].in(['foo'])
39
+ end
40
+
41
+ it 'should be able to find with LIKE' do
42
+ @comparator.like?('foo').arel.should == @table[:name].matches('foo')
43
+ @comparator.matches?('foo').arel.should == @table[:name].matches('foo')
44
+ (@comparator =~ 'foo').arel.should == @table[:name].matches('foo')
45
+ end
46
+
47
+ #it 'should be able to find with Regex'
48
+ end
@@ -0,0 +1,87 @@
1
+ require "spec/helper"
2
+
3
+ describe ArelOperators::Finder do
4
+ before do
5
+ @table = Person.arel_table
6
+ @finder = ArelOperators::Finder.new Person.arel_table
7
+ end
8
+
9
+ it 'should be able to convert to a arel' do
10
+ arel = mock("Arel")
11
+ @finder = ArelOperators::Finder.new Person.arel_table, arel
12
+ @finder.arel.should == arel
13
+ end
14
+
15
+ it 'should return a empty arel if not being able to convert' do
16
+ @finder.arel.should be_nil
17
+ end
18
+
19
+ it 'should not calculate equality' do
20
+ proc { @finder == @finder }.should raise_error(NoMethodError)
21
+ proc { @finder != @finder }.should raise_error(NoMethodError)
22
+ end
23
+
24
+ it 'should be able to expose the class attributes' do
25
+ @finder.name.should be_a_kind_of(ArelOperators::Finder::Comparators)
26
+ @finder.id.should be_a_kind_of(ArelOperators::Finder::Comparators)
27
+ end
28
+
29
+ it 'should be able to "or" two conditions' do
30
+ c1 = @table[:id].eq(1)
31
+ c2 = @table[:name].eq(2)
32
+ ((@finder.id == 1) | (@finder.name == 2)).arel.should ==
33
+ c1.or(c2)
34
+ end
35
+
36
+ it 'should be able to "and" two conditions' do
37
+ c1 = @table[:id].eq(1)
38
+ c2 = @table[:name].eq(2)
39
+ ((@finder.id == 1) & (@finder.name == 2)).arel.should ==
40
+ c1.and(c2)
41
+ end
42
+
43
+ it 'should be able to negate the find (in Ruby1.9)' do
44
+ next if RUBY_VERSION < '1.9.0'
45
+ @finder = ArelOperators::Finder.new Person.arel_table, @table[:name].eq('foo')
46
+ (!@finder).arel.should ==
47
+ Arel::Predicates::Not.new(@table[:name].eq('foo'))
48
+ end
49
+
50
+ it 'should be able to negate the find with -@' do
51
+ @finder = ArelOperators::Finder.new Person.arel_table, @table[:name].eq('foo')
52
+ (-@finder).arel.should ==
53
+ Arel::Predicates::Not.new(@table[:name].eq('foo'))
54
+ end
55
+
56
+ #it 'should be able to find in a subselect'
57
+
58
+ context 'when finding using WHERE and a block' do
59
+ before do
60
+ @p1 = Person.create! :name => 'Foo', :age => 17
61
+ @p2 = Person.create! :name => 'Foo', :age => 18
62
+ end
63
+
64
+ after do
65
+ Person.delete_all
66
+ end
67
+
68
+ it 'should be able to find using a block with one parameter' do
69
+ result = Person.where { |p| (p.name == 'Foo') & (p.age > 17) }
70
+ result.should == [@p2]
71
+ end
72
+
73
+ it 'should be able to find using a block with no parameters' do
74
+ result = Person.where { (name == 'Foo') & (age > 17) }
75
+ result.should == [@p2]
76
+ end
77
+
78
+ it 'should be able to find using a condition too' do
79
+ result = Person.where(:age => 17) { name == 'Foo' }
80
+ result.should == [@p1]
81
+ end
82
+
83
+ it 'should be able to find on a HAVING condition' do
84
+ pending
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../helper')
2
+
3
+ describe ArelOperators::Operators do
4
+ before do
5
+ @p1 = Person.create! :name => 'Foo'
6
+ @p2 = Person.create! :name => 'Bar'
7
+ @p3 = Person.create! :name => 'Baz'
8
+ Address.create! :address => "Avenue", :person_id => @p2.id
9
+ Address.create! :address => "Road", :person_id => @p3.id
10
+ end
11
+
12
+ after do
13
+ Person.delete_all; Address.delete_all
14
+ end
15
+
16
+ it 'should obey joins' do
17
+ Address.create! :address => "Village", :person_id => @p1.id
18
+ q1 = Person.where :name => 'Foo'
19
+ q2 = Person.joins(:addresses).where('addresses.address = ?', 'Road')
20
+ result = q2 | q1
21
+ result.to_sql.should match(/inner join/i)
22
+ result.should include(@p1)
23
+ result.should include(@p3)
24
+ result.should_not include(@p2)
25
+ end
26
+
27
+ it 'should obey left joins' do
28
+ pending 'Left Join in Rails?'
29
+ q1 = Person.where :name => 'Foo'
30
+ q2 = Person.eager_load(:addresses).where('addresses.address = ?', 'Road')
31
+ puts q2.to_sql
32
+ result = q1 | q2
33
+ result.to_sql.should match(/join/i)
34
+ result.should include(@p1)
35
+ result.should include(@p3)
36
+ result.should_not include(@p2)
37
+ end
38
+ end
@@ -1,7 +1,6 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../helper')
2
- require "ruby_debug"
3
2
 
4
- describe ActiveRecord::Operators do
3
+ describe ArelOperators::Operators do
5
4
  it 'Should "or" two conditions' do
6
5
  arel = Person.where(:id => 190) | Person.where(:id => 210)
7
6
  result = arel.to_sql
@@ -11,7 +10,7 @@ describe ActiveRecord::Operators do
11
10
  end
12
11
 
13
12
  it 'Should "and" two conditions' do
14
- arel = Person.where(:id => 190) & Person.where(:id => 210)
13
+ arel = Person.where(:id => 190).and Person.where(:id => 210)
15
14
  result = arel.to_sql
16
15
  result.should match(/and/i)
17
16
  result.should match(/210/i)
@@ -28,6 +27,7 @@ describe ActiveRecord::Operators do
28
27
  it 'should subtract two conditions' do
29
28
  arel = Person.where(:id => 190) - Person.where(:id => 210)
30
29
  result = arel.to_sql
30
+ result.should have(1).select_clause
31
31
  result.should match(/and.*not/i)
32
32
  result.should match(/210/i)
33
33
  result.should match(/190/i)
@@ -43,7 +43,6 @@ describe ActiveRecord::Operators do
43
43
  result.should include(baz)
44
44
  result.should_not include(foo)
45
45
  result.should_not include(bar)
46
- sql = result.to_sql
47
- sql.scan(/select/i).should have(2).matches
46
+ result.to_sql.should have(2).select_clauses
48
47
  end
49
48
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 1
7
8
  - 0
8
- - 2
9
- version: 0.0.2
9
+ version: 0.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - "Maur\xC3\xADcio Szabo"
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-09-01 00:00:00 -03:00
17
+ date: 2010-09-19 00:00:00 -03:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -42,9 +42,14 @@ extra_rdoc_files:
42
42
  - README
43
43
  files:
44
44
  - lib/arel_operators.rb
45
- - lib/active_record/operators.rb
45
+ - lib/arel_operators/finder.rb
46
+ - lib/arel_operators/finder/comparators.rb
47
+ - lib/arel_operators/operators.rb
46
48
  - README
47
- - spec/active_record/operators_spec.rb
49
+ - spec/arel_operators/joins_spec.rb
50
+ - spec/arel_operators/operators_spec.rb
51
+ - spec/arel_operators/finder/comparators_spec.rb
52
+ - spec/arel_operators/finder_spec.rb
48
53
  has_rdoc: true
49
54
  homepage: http://github.com/mauricioszabo/arel_operators
50
55
  licenses: []
@@ -78,4 +83,7 @@ signing_key:
78
83
  specification_version: 3
79
84
  summary: Operators (|, &, -) for ActiveRecord.
80
85
  test_files:
81
- - spec/active_record/operators_spec.rb
86
+ - spec/arel_operators/joins_spec.rb
87
+ - spec/arel_operators/operators_spec.rb
88
+ - spec/arel_operators/finder/comparators_spec.rb
89
+ - spec/arel_operators/finder_spec.rb
@@ -1,53 +0,0 @@
1
- module ActiveRecord
2
- module Operators
3
- def |(other)
4
- build_arel_conditions build_predicate(Arel::Predicates::Or, where_values, other.where_values)
5
- end
6
-
7
- def &(other)
8
- build_arel_conditions build_predicate(Arel::Predicates::And, where_values, other.where_values)
9
- end
10
-
11
- def -(other)
12
- self & (-other)
13
- end
14
-
15
- def -@
16
- build_arel_conditions build_predicate(Arel::Predicates::Not, where_values)
17
- end
18
-
19
- def where(obj, *args)
20
- if obj.respond_to?(:build_where)
21
- relation = obj.select(:id)
22
- self & build_arel_conditions(Arel::Predicates::In.new(relation.primary_key, relation.arel))
23
- else
24
- super
25
- end
26
- end
27
-
28
- private
29
- def build_arel_conditions(condition)
30
- clone.tap do |c|
31
- cond = build_where(condition)
32
- c.where_values = Array.wrap(cond)
33
- end
34
- end
35
-
36
- def build_predicate(predicate, *values)
37
- values = values.collect do |v|
38
- wrap_predicate_value(v)
39
- end
40
- predicate.new(*values)
41
- end
42
-
43
- def wrap_predicate_value(value)
44
- value.collect do |v|
45
- if v.is_a?(String)
46
- Arel::SqlLiteral.new(v)
47
- else
48
- v
49
- end
50
- end
51
- end
52
- end
53
- end