activerecord_finder 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +95 -0
- data/lib/activerecord_finder/comparator.rb +34 -0
- data/lib/activerecord_finder/finder.rb +34 -0
- data/lib/activerecord_finder/where.rb +22 -0
- data/lib/activerecord_finder.rb +5 -0
- data/spec/activerecord_finder/comparator_spec.rb +44 -0
- data/spec/activerecord_finder/finder_spec.rb +32 -0
- data/spec/activerecord_finder/where_spec.rb +38 -0
- metadata +73 -0
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
Arel Finders
|
2
|
+
============
|
3
|
+
|
4
|
+
The problem with ActiveRecord today is that there is no way to
|
5
|
+
construct queries outside "attribute = value" or using "OR" operator.
|
6
|
+
The solutions so far are not so easy to do, and usualy involves creating
|
7
|
+
fragments of SQL in string form.
|
8
|
+
|
9
|
+
But we have Arel. So, this must be solved, right?
|
10
|
+
|
11
|
+
Unfortunately, no. Arel's API is quite dificult to use, and its API is quite
|
12
|
+
inconsistent and changes over time, resulting in difficult code to mantain.
|
13
|
+
|
14
|
+
## ActiveRecord::Finder
|
15
|
+
|
16
|
+
So, the solution was to create an object, an "adapter-kind" of object.
|
17
|
+
ActiveRecord::Finder is a kind of object that contains all attributes
|
18
|
+
from a table, and allows you to `OR` or `AND` queries.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
finder = ActiveRecord::Finder.new(Arel::Table.new(:users))
|
22
|
+
query = finder.age > 10 | finder.name == "Foo"
|
23
|
+
query.arel.to_sql
|
24
|
+
#Returns: users.age > 10 OR users.name = 'Foo'
|
25
|
+
```
|
26
|
+
|
27
|
+
But it is kind of tedious to do this everytime you want to search for users,
|
28
|
+
so you can use ActiveRecord::Base#restrict with a block to construct your queries.
|
29
|
+
|
30
|
+
## ActiveRecord's Finders
|
31
|
+
|
32
|
+
Integrating with ActiveRecord, it is possible to use `restrict` to find records on a ActiveRecord object. The syntax is quite simple:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
User.restrict { |f| f.age > 18 }
|
36
|
+
#or
|
37
|
+
User.restrict { age > 18 }
|
38
|
+
```
|
39
|
+
|
40
|
+
ANDs work on the same way, too:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
User.restrict { age > 18 & age < 20 }
|
44
|
+
```
|
45
|
+
|
46
|
+
### All finders, so far:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
#WHERE NOT (users.age < 10)
|
50
|
+
User.restrict { !(age < 10) }
|
51
|
+
|
52
|
+
#Comparission
|
53
|
+
User.restrict { age == 20 }
|
54
|
+
User.restrict { age >= 20 }
|
55
|
+
User.restrict { age <= 20 }
|
56
|
+
User.restrict { age != 20 }
|
57
|
+
User.restrict { age > 20 }
|
58
|
+
User.restrict { age < 20 }
|
59
|
+
|
60
|
+
#IN
|
61
|
+
User.restrict { age.in?([10, 20, 30]) }
|
62
|
+
|
63
|
+
#LIKE
|
64
|
+
User.restrict { name =~ "Foo%" }
|
65
|
+
User.restrict { name !~ "Foo%" }
|
66
|
+
|
67
|
+
#AND, and OR
|
68
|
+
User.restrict { (name == "Foo") & (age < 20) }
|
69
|
+
User.restrict { (name == "Foo") | (age < 30) }
|
70
|
+
User.restrict do
|
71
|
+
( (name == "Foo") & (age < 20) ) |
|
72
|
+
( (name == "Bar") | (age > 20) )
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Plase note that because of operator precedence in Ruby, we must add a parenthesis over the comparission before using `&` or `|`.
|
77
|
+
|
78
|
+
## Constructing Queries
|
79
|
+
|
80
|
+
To avoid "merging" ActiveRecord::Relation's, there is a specific syntax to construct a complex query on ActiveRecord::Finder.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
def search(name_to_search, params = {})
|
84
|
+
query = User.new_finder { name == name_to_search }
|
85
|
+
query = query & User.new_finder { age > params[:age] } if params.include?(:age)
|
86
|
+
query = query | User.new_finder { age <= 10 } if params[:include_all_childs]
|
87
|
+
User.restrict(query)
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
### Using "joins"
|
92
|
+
|
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`
|
94
|
+
|
95
|
+
##
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecordFinder
|
2
|
+
class Comparator
|
3
|
+
def initialize(finder, field)
|
4
|
+
@finder = finder
|
5
|
+
@field = field
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.convert_to_arel(operation, arel_method)
|
9
|
+
define_method(operation) do |other|
|
10
|
+
arel_clause = @field.send(arel_method, other)
|
11
|
+
Finder.new(@finder.table, arel_clause)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def nil?
|
16
|
+
self == nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def in?(other)
|
20
|
+
other = other.arel if other.respond_to?(:arel)
|
21
|
+
arel_clause = @field.in(other)
|
22
|
+
Finder.new(@finder.table, arel_clause)
|
23
|
+
end
|
24
|
+
|
25
|
+
convert_to_arel :==, :eq
|
26
|
+
convert_to_arel '!=', :not_eq
|
27
|
+
convert_to_arel :>, :gt
|
28
|
+
convert_to_arel :>=, :gteq
|
29
|
+
convert_to_arel :<, :lt
|
30
|
+
convert_to_arel :<=, :lteq
|
31
|
+
convert_to_arel :=~, :matches
|
32
|
+
convert_to_arel '!~', :does_not_match
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecordFinder
|
2
|
+
class Finder
|
3
|
+
attr_reader :table, :arel
|
4
|
+
|
5
|
+
def initialize(arel_table, operation=nil)
|
6
|
+
@table = arel_table
|
7
|
+
@arel = operation
|
8
|
+
@table.columns.each { |a| define_attribute_method a }
|
9
|
+
end
|
10
|
+
|
11
|
+
def define_attribute_method(arel_attribute)
|
12
|
+
singleton_class.send :define_method, arel_attribute.name do
|
13
|
+
Comparator.new(self, arel_attribute)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
private :define_attribute_method
|
17
|
+
|
18
|
+
def |(other)
|
19
|
+
other = other.arel if other.respond_to?(:arel)
|
20
|
+
Finder.new(table, arel.or(other))
|
21
|
+
end
|
22
|
+
|
23
|
+
def &(other)
|
24
|
+
other = other.arel if other.respond_to?(:arel)
|
25
|
+
Finder.new(table, arel.and(other))
|
26
|
+
end
|
27
|
+
|
28
|
+
def !@
|
29
|
+
Finder.new(table, Arel::Nodes::Not.new(arel))
|
30
|
+
end
|
31
|
+
|
32
|
+
undef_method :==
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecordFinder
|
2
|
+
module Where
|
3
|
+
def restrict(finder = nil, &block)
|
4
|
+
raise ArgumentError, 'wrong number of arguments (0 for 1)' if block.nil? && finder.nil?
|
5
|
+
if block
|
6
|
+
where(new_finder(&block).arel)
|
7
|
+
else
|
8
|
+
where(finder.arel)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
alias :condition :restrict
|
12
|
+
|
13
|
+
def new_finder(&block)
|
14
|
+
activerecord_finder = ActiveRecordFinder::Finder.new(arel_table)
|
15
|
+
if block.arity == 1
|
16
|
+
block.call(activerecord_finder)
|
17
|
+
else
|
18
|
+
activerecord_finder.instance_eval(&block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
describe ActiveRecordFinder::Comparator do
|
4
|
+
subject { ActiveRecordFinder::Comparator.new(finder, table[:name]) }
|
5
|
+
let(:finder) { ActiveRecordFinder::Finder.new(table) }
|
6
|
+
let(:table) { Person.arel_table }
|
7
|
+
|
8
|
+
it 'should be able to find by equality' do
|
9
|
+
(subject == 'foo').should be_a_kind_of(ActiveRecordFinder::Finder)
|
10
|
+
(subject == 'foo').arel.should be_equivalent_to(table[:name].eq('foo'))
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should be able to find by inequality' do
|
14
|
+
(subject != 'foo').arel.should be_equivalent_to(table[:name].not_eq('foo'))
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should be able to verify if an attribute is nil' do
|
18
|
+
(subject.nil?).arel.should be_equivalent_to(table[:name].eq(nil))
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be able to find by greater than' do
|
22
|
+
(subject > 'foo').arel.should be_equivalent_to table[:name].gt('foo')
|
23
|
+
(subject >= 'foo').arel.should be_equivalent_to table[:name].gteq('foo')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should be able to find by lower than' do
|
27
|
+
(subject < 'foo').arel.should be_equivalent_to table[:name].lt('foo')
|
28
|
+
(subject <= 'foo').arel.should be_equivalent_to table[:name].lteq('foo')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'finds with IN' do
|
32
|
+
subject.in?(['foo']).arel.should be_equivalent_to table[:name].in(['foo'])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'finds with subselects' do
|
36
|
+
result = subject.in?(Person.where(age: 10))
|
37
|
+
result.arel.to_sql.should match(/select/i)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should be able to find with LIKE' do
|
41
|
+
(subject =~ 'foo').arel.should be_equivalent_to table[:name].matches('foo')
|
42
|
+
(subject !~ 'foo').arel.should be_equivalent_to table[:name].does_not_match('foo')
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
describe ActiveRecordFinder::Finder do
|
4
|
+
subject { ActiveRecordFinder::Finder.new(table) }
|
5
|
+
let(:table) { Person.arel_table }
|
6
|
+
|
7
|
+
it 'should return a empty arel if not being able to convert' do
|
8
|
+
subject.arel.should be_nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should not calculate equality' do
|
12
|
+
proc { subject == subject }.should raise_error(NoMethodError)
|
13
|
+
proc { subject != subject }.should raise_error(NoMethodError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should be able to "or" two conditions' do
|
17
|
+
c1 = table[:id].eq(1)
|
18
|
+
c2 = table[:name].eq(2)
|
19
|
+
((subject.id == 1) | (subject.name == 2)).arel.should be_equivalent_to c1.or(c2)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should be able to "and" two conditions' do
|
23
|
+
c1 = table[:id].eq(1)
|
24
|
+
c2 = table[:name].eq(2)
|
25
|
+
((subject.id == 1) & (subject.name == 2)).arel.should be_equivalent_to c1.and(c2)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should be able to negate the find' do
|
29
|
+
result = subject.name == 'foo'
|
30
|
+
(!result).arel.should be_equivalent_to Arel::Nodes::Not.new(table[:name].eq('foo'))
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
describe ActiveRecordFinder::Where do
|
4
|
+
let!(:bar) { Person.create! :name => 'Bar', :age => 17 }
|
5
|
+
let!(:seventeen) { Person.create! :name => 'Foo', :age => 17 }
|
6
|
+
let!(:eighteen) { Person.create! :name => 'Foo', :age => 18 }
|
7
|
+
|
8
|
+
after do
|
9
|
+
Person.delete_all
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'finds using a block with one parameter' do
|
13
|
+
result = Person.restrict { |p| (p.name == 'Foo') & (p.age > 17) }
|
14
|
+
result.should == [eighteen]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'finds using a block with no parameters' do
|
18
|
+
result = Person.restrict { (name == 'Foo') & (age > 17) }
|
19
|
+
result.should == [eighteen]
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'concatenates conditions' do
|
23
|
+
finder1 = Person.new_finder { age == 17 }
|
24
|
+
finder2 = Person.new_finder { name == "Foo" }
|
25
|
+
result = Person.restrict(finder1 & finder2)
|
26
|
+
result.should == [seventeen]
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'concatenates arel conditions' do
|
30
|
+
pending
|
31
|
+
finder1 = Person.where(name: "Foo").arel
|
32
|
+
finder2 = Person.new_finder { age == 17 }
|
33
|
+
result = Person.where((finder2 & finder1).arel)
|
34
|
+
result.should == [seventeen]
|
35
|
+
result = Person.where((finder1 & finder2).arel)
|
36
|
+
result.should == [seventeen]
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_finder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Maurício Szabo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.2'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.2'
|
30
|
+
description:
|
31
|
+
email: mauricio.szabo@gmail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files:
|
35
|
+
- README.md
|
36
|
+
files:
|
37
|
+
- lib/activerecord_finder/where.rb
|
38
|
+
- lib/activerecord_finder/finder.rb
|
39
|
+
- lib/activerecord_finder/comparator.rb
|
40
|
+
- lib/activerecord_finder.rb
|
41
|
+
- README.md
|
42
|
+
- spec/activerecord_finder/finder_spec.rb
|
43
|
+
- spec/activerecord_finder/comparator_spec.rb
|
44
|
+
- spec/activerecord_finder/where_spec.rb
|
45
|
+
homepage: http://github.com/mauricioszabo/arel_operators
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.25
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Better finder syntax (|, &, >=, <=) for ActiveRecord.
|
69
|
+
test_files:
|
70
|
+
- spec/activerecord_finder/finder_spec.rb
|
71
|
+
- spec/activerecord_finder/comparator_spec.rb
|
72
|
+
- spec/activerecord_finder/where_spec.rb
|
73
|
+
has_rdoc: true
|