activerecord_finder 0.1.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 +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
|