active_record_query 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +150 -0
- data/Rakefile +19 -0
- data/active_record_query.gemspec +25 -0
- data/lib/active_record/query.rb +57 -0
- data/lib/active_record/query/context.rb +39 -0
- data/lib/active_record/query/subject.rb +67 -0
- data/lib/active_record_query.rb +4 -0
- data/lib/active_record_query/relation_extension.rb +33 -0
- data/lib/active_record_query/version.rb +3 -0
- data/test/helper.rb +4 -0
- data/test/integration/query_test.rb +99 -0
- data/test/unit/query/subject_test.rb +108 -0
- data/test/unit/query_test.rb +57 -0
- data/test/unit/relation_extension_test.rb +55 -0
- metadata +100 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# Active Record Query Plugin #
|
2
|
+
|
3
|
+
Imaginatively named, I know.
|
4
|
+
|
5
|
+
This is a proof-of-concept I want to be considered for inclusion in
|
6
|
+
Rails 4.
|
7
|
+
|
8
|
+
## Synopsis ##
|
9
|
+
|
10
|
+
``` ruby
|
11
|
+
Person.where { |q| q.name == 'Jon' && q.age == 22 }
|
12
|
+
# => SELECT * FROM people WHERE people.name = 'Jon' AND people.age = 22
|
13
|
+
|
14
|
+
Person.any { |q| q.name == 'Jon' || q.age == 22 }
|
15
|
+
# => SELECT * FROM people WHERE people.name = 'Jon' OR people.age = 22
|
16
|
+
|
17
|
+
Person.joins(:projects).where { |q| q.projects.name == 'Rails' }
|
18
|
+
# => SELECT * FROM people INNER JOIN projects WHERE projecs.name = 'Rails'
|
19
|
+
|
20
|
+
Person.any { |q| q.name = 'Jon' || q.and { q.age >= 10 && q.age < 30 } }
|
21
|
+
# => SELECT * FROM people WHERE people.name = 'Jon' OR (people.age >= 10 AND people.age < 30)
|
22
|
+
```
|
23
|
+
|
24
|
+
## Why? ##
|
25
|
+
|
26
|
+
* Makes people (who use it) less vulnerable to accidentally introducing
|
27
|
+
SQL injection points
|
28
|
+
|
29
|
+
* If we have the AST, we can draw inferences from it in Active Record.
|
30
|
+
For example, in Rails 4,
|
31
|
+
|
32
|
+
``` ruby
|
33
|
+
Post.includes(:comments).where('comments.created_at > x')
|
34
|
+
```
|
35
|
+
|
36
|
+
will no longer `JOIN` the `comments` table. You have to do:
|
37
|
+
|
38
|
+
``` ruby
|
39
|
+
Post.includes(:comments).where('comments.created_at > x').references(:comments)
|
40
|
+
```
|
41
|
+
|
42
|
+
With the AST available to us, we can automatically infer that
|
43
|
+
`comments` is referenced.
|
44
|
+
|
45
|
+
## Prior art ##
|
46
|
+
|
47
|
+
* https://github.com/ernie/squeel
|
48
|
+
* http://defunkt.io/ambition/
|
49
|
+
|
50
|
+
## Design goals ##
|
51
|
+
|
52
|
+
* Don't use `instance_eval`. Here be dragons.
|
53
|
+
|
54
|
+
* Make the syntax as easy on the eyes as possible.
|
55
|
+
|
56
|
+
`&&` and `||`
|
57
|
+
cannot be redefined as methods (more about that below), but `&` and
|
58
|
+
`|` can be. However, they bind tighter than comparison operators,
|
59
|
+
resulting in lots of unpleasant parentheses:
|
60
|
+
|
61
|
+
``` ruby
|
62
|
+
Person.where { |q| (q.name == 'Jon') & (q.age == 22) }
|
63
|
+
```
|
64
|
+
|
65
|
+
`&` and `|` are also commonly used for set operations, which have an
|
66
|
+
existing meaning in SQL.
|
67
|
+
|
68
|
+
## Implementation ##
|
69
|
+
|
70
|
+
The `q` object is *mutable*.
|
71
|
+
|
72
|
+
Writing,
|
73
|
+
|
74
|
+
``` ruby
|
75
|
+
Post.where { |q| q.name == 'Jon' && q.age == 22 }
|
76
|
+
```
|
77
|
+
|
78
|
+
has the identical effect as writing,
|
79
|
+
|
80
|
+
``` ruby
|
81
|
+
Post.where do |q|
|
82
|
+
q.name == 'Jon'
|
83
|
+
q.age == 22
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
The `==` method return `true` to prevent the `&&` operator from
|
88
|
+
short-circuiting. Using `&&` is purely syntactical sugar.
|
89
|
+
|
90
|
+
Note that,
|
91
|
+
|
92
|
+
``` ruby
|
93
|
+
Post.where { |q| q.name == 'Jon' || q.age == 22 }
|
94
|
+
```
|
95
|
+
|
96
|
+
*would* short-circuit.
|
97
|
+
|
98
|
+
So whilst this wouldn't throw an error, it at
|
99
|
+
least would result in something that the user would (hopefully) notice
|
100
|
+
is wrong quite quickly (because `age` would be missing from the query
|
101
|
+
entirely).
|
102
|
+
|
103
|
+
This 'hack' is definitely the worst thing about the idea, but I think
|
104
|
+
that with adaquate documentation it wouldn't pose too much problem, and
|
105
|
+
it reads quite naturally.
|
106
|
+
|
107
|
+
## Supported operators ##
|
108
|
+
|
109
|
+
``` ruby
|
110
|
+
q.name == 'Jon'
|
111
|
+
q.name != 'Jon'
|
112
|
+
q.name =~ 'J%'
|
113
|
+
q.name !~ 'J%'
|
114
|
+
q.name > 22
|
115
|
+
q.name < 22
|
116
|
+
q.name >= 22
|
117
|
+
q.name <= 22
|
118
|
+
q.name.in ['Jon', 'Emily']
|
119
|
+
q.name.not_in ['Jon', 'Emily']
|
120
|
+
```
|
121
|
+
|
122
|
+
## Possible alternative syntaxes ##
|
123
|
+
|
124
|
+
### Option 1 (Squeel syntax) ###
|
125
|
+
|
126
|
+
``` ruby
|
127
|
+
Person.where { |q| (q.name == 'Jon') | (q.age == 22) }
|
128
|
+
```
|
129
|
+
|
130
|
+
* Doesn't require the `q.and { ... }` thing for `AND`-within-`OR` or
|
131
|
+
`OR`-within-`AND`
|
132
|
+
* Possibly confusing use of set operators
|
133
|
+
* Lots of parentheses
|
134
|
+
|
135
|
+
### Option 2 ###
|
136
|
+
|
137
|
+
``` ruby
|
138
|
+
Person.where { |q| q.name == 'Jon'; q.age == 22 }
|
139
|
+
```
|
140
|
+
|
141
|
+
* This works already, it's a question of what we advocate / document.
|
142
|
+
|
143
|
+
### Option 3 ###
|
144
|
+
|
145
|
+
``` ruby
|
146
|
+
Person.any { |q| q[:name] == 'Jon' || q.projects[:name] == 'Rails' }
|
147
|
+
```
|
148
|
+
|
149
|
+
* Draws a clearer distinction between table and column names
|
150
|
+
* A bit more noisy
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
namespace :test do
|
5
|
+
Rake::TestTask.new(:unit) do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList['test/unit/**/*_test.rb']
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:integration) do |t|
|
12
|
+
t.libs << "test"
|
13
|
+
t.test_files = FileList['test/integration/**/*_test.rb']
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
task :test => ['test:unit', 'test:integration']
|
19
|
+
task :default => :test
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "active_record_query/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "active_record_query"
|
7
|
+
s.version = ARQuery::VERSION
|
8
|
+
s.authors = ["Jon Leighton"]
|
9
|
+
s.email = ["j@jonathanleighton.com"]
|
10
|
+
s.homepage = "https://github.com/jonleighton/active_record_query"
|
11
|
+
s.summary = %q{Proof of concept for proposed new Active Record query API}
|
12
|
+
s.description = %q{Proof of concept for proposed new Active Record query API}
|
13
|
+
|
14
|
+
s.rubyforge_project = "ar_query"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'activerecord'
|
22
|
+
|
23
|
+
s.add_development_dependency 'minitest'
|
24
|
+
s.add_development_dependency 'mocha'
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'arel'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Query < BasicObject
|
5
|
+
::Kernel.require 'active_record/query/subject'
|
6
|
+
::Kernel.require 'active_record/query/context'
|
7
|
+
|
8
|
+
CONTEXT = {
|
9
|
+
:or => Or,
|
10
|
+
:and => And
|
11
|
+
}
|
12
|
+
|
13
|
+
attr_reader :table
|
14
|
+
|
15
|
+
def initialize(table, context)
|
16
|
+
@table = table
|
17
|
+
@nodes = []
|
18
|
+
@stack = [CONTEXT[context].new]
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(name, *args, &block)
|
22
|
+
if args.empty? && !block
|
23
|
+
Subject.new(self, name)
|
24
|
+
else
|
25
|
+
::Kernel.raise ::ArgumentError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to?(name, include_private = nil)
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def and
|
34
|
+
@stack << And.new
|
35
|
+
yield
|
36
|
+
self << @stack.pop.arel
|
37
|
+
end
|
38
|
+
|
39
|
+
def or
|
40
|
+
@stack << Or.new
|
41
|
+
yield
|
42
|
+
self << @stack.pop.arel
|
43
|
+
end
|
44
|
+
|
45
|
+
def <<(node)
|
46
|
+
@stack.last << node
|
47
|
+
end
|
48
|
+
|
49
|
+
def arel
|
50
|
+
@stack.last.arel
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<ActiveRecord::Query table=#{table.inspect}>"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class ActiveRecord::Query
|
2
|
+
class Context
|
3
|
+
attr_reader :nodes
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@nodes = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(node)
|
10
|
+
@nodes << node
|
11
|
+
end
|
12
|
+
|
13
|
+
def arel
|
14
|
+
Arel::Nodes::Grouping.new(nodes)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Or < Context
|
19
|
+
def nodes
|
20
|
+
@nodes.inject { |mem, node| Arel::Nodes::Or.new(mem, node) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def <<(node)
|
24
|
+
super
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class And < Context
|
30
|
+
def nodes
|
31
|
+
Arel::Nodes::And.new(@nodes)
|
32
|
+
end
|
33
|
+
|
34
|
+
def <<(node)
|
35
|
+
super
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class ActiveRecord::Query
|
2
|
+
class Subject < BasicObject
|
3
|
+
def initialize(owner, name, table = nil)
|
4
|
+
@owner = owner
|
5
|
+
@name = name
|
6
|
+
@table = table || @owner.table
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(method_name, *args, &block)
|
10
|
+
if args.empty? && !block
|
11
|
+
Subject.new(@owner, method_name, ::Arel::Table.new(@name))
|
12
|
+
else
|
13
|
+
::Kernel.raise ::ArgumentError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to?(name, include_private = nil)
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(value)
|
22
|
+
__add__ :eq, value
|
23
|
+
end
|
24
|
+
|
25
|
+
def !=(value)
|
26
|
+
__add__ :not_eq, value
|
27
|
+
end
|
28
|
+
|
29
|
+
def =~(value)
|
30
|
+
__add__ :matches, value
|
31
|
+
end
|
32
|
+
|
33
|
+
def !~(value)
|
34
|
+
__add__ :does_not_match, value
|
35
|
+
end
|
36
|
+
|
37
|
+
def >(value)
|
38
|
+
__add__ :gt, value
|
39
|
+
end
|
40
|
+
|
41
|
+
def <(value)
|
42
|
+
__add__ :lt, value
|
43
|
+
end
|
44
|
+
|
45
|
+
def >=(value)
|
46
|
+
__add__ :gteq, value
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=(value)
|
50
|
+
__add__ :lteq, value
|
51
|
+
end
|
52
|
+
|
53
|
+
def in(value)
|
54
|
+
__add__ :in, value
|
55
|
+
end
|
56
|
+
|
57
|
+
def not_in(value)
|
58
|
+
__add__ :not_in, value
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def __add__(operator, value)
|
64
|
+
@owner << @table[@name].send(operator, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
3
|
+
module ActiveRecordQuery
|
4
|
+
module RelationExtension
|
5
|
+
def where(*args)
|
6
|
+
if args.empty? && block_given?
|
7
|
+
query = ActiveRecord::Query.new(table, :and)
|
8
|
+
yield query
|
9
|
+
super query.arel
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def any
|
16
|
+
query = ActiveRecord::Query.new(table, :or)
|
17
|
+
yield query
|
18
|
+
where(query.arel)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if defined?(ActiveRecord::Base)
|
24
|
+
require 'active_record/relation'
|
25
|
+
|
26
|
+
class ActiveRecord::Relation
|
27
|
+
include ActiveRecordQuery::RelationExtension
|
28
|
+
end
|
29
|
+
|
30
|
+
class << ActiveRecord::Base
|
31
|
+
delegate :any, :to => :scoped
|
32
|
+
end
|
33
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
5
|
+
|
6
|
+
ActiveRecord::Migration.verbose = false
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
create_table :posts do |t|
|
9
|
+
t.string :title
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :comments do |t|
|
13
|
+
t.references :post
|
14
|
+
t.string :author
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Post < ActiveRecord::Base
|
19
|
+
has_many :comments
|
20
|
+
end
|
21
|
+
|
22
|
+
class Comment < ActiveRecord::Base
|
23
|
+
end
|
24
|
+
|
25
|
+
module ActiveRecord
|
26
|
+
class QueryIntegrationTest < MiniTest::Unit::TestCase
|
27
|
+
def setup
|
28
|
+
@hello = Post.create(title: 'Hello')
|
29
|
+
@goodbye = Post.create(title: 'Goodbye')
|
30
|
+
|
31
|
+
@hello.comments.create(author: 'Jon')
|
32
|
+
end
|
33
|
+
|
34
|
+
def teardown
|
35
|
+
Post.delete_all
|
36
|
+
end
|
37
|
+
|
38
|
+
# Checks that none of the operators blow up
|
39
|
+
def test_operators
|
40
|
+
Post.where do |q|
|
41
|
+
q.title == 'x'
|
42
|
+
q.title != 'x'
|
43
|
+
q.title =~ 'x'
|
44
|
+
q.title !~ 'x'
|
45
|
+
q.title > 'x'
|
46
|
+
q.title < 'x'
|
47
|
+
q.title >= 'x'
|
48
|
+
q.title <= 'x'
|
49
|
+
q.title.in 'x'
|
50
|
+
q.title.not_in 'x'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_basic_query
|
55
|
+
assert_equal [@hello], Post.where { |q| q.title == 'Hello' }
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_and_query
|
59
|
+
assert_equal [@hello], Post.where { |q| q.title == 'Hello' && q.title =~ 'Hell%' }
|
60
|
+
assert_equal [], Post.where { |q| q.title == 'Hello' && q.title == 'Goodbye' }
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_or_query
|
64
|
+
assert_equal [@hello, @goodbye], Post.any { |q| q.title == 'Hello' || q.title == 'Goodbye' }
|
65
|
+
assert_equal [@hello], Post.any { |q| q.title == 'Hello' || q.title == 'non-existent' }
|
66
|
+
assert_equal [@hello], Post.any { |q| q.title == 'non-existent' || q.title == 'Hello' }
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_other_table_query
|
70
|
+
assert_equal [@hello], Post.joins(:comments).where { |q| q.comments.author == 'Jon' }
|
71
|
+
assert_equal [], Post.joins(:comments).where { |q| q.comments.author == 'Bob' }
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_nested_contexts
|
75
|
+
query = Post.any { |q|
|
76
|
+
q.title == 'non-existent' ||
|
77
|
+
q.and {
|
78
|
+
q.title == 'Hello' &&
|
79
|
+
q.or {
|
80
|
+
q.title == 'other' ||
|
81
|
+
q.title =~ 'Hell%'
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
assert query.to_sql.include?(
|
87
|
+
%q{("posts"."title" = 'non-existent' OR ("posts"."title" = 'Hello' AND ("posts"."title" = 'other' OR "posts"."title" LIKE 'Hell%')))}
|
88
|
+
)
|
89
|
+
|
90
|
+
assert_equal [@hello], query
|
91
|
+
|
92
|
+
query = Post.any { |q| q.and { q.title == 'a' && q.title == 'b' } || q.title == 'c' }
|
93
|
+
|
94
|
+
assert query.to_sql.include?(
|
95
|
+
%q{(("posts"."title" = 'a' AND "posts"."title" = 'b') OR "posts"."title" = 'c')}
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class SubjectTest < MiniTest::Unit::TestCase
|
4
|
+
class Column
|
5
|
+
attr_reader :table, :name
|
6
|
+
|
7
|
+
def initialize(table, name)
|
8
|
+
@table = table
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.class == self.class &&
|
14
|
+
other.table == table &&
|
15
|
+
other.name == name
|
16
|
+
end
|
17
|
+
|
18
|
+
PREDICATES = [:eq, :not_eq, :matches, :does_not_match, :gt, :lt, :gteq, :lteq, :in, :not_in]
|
19
|
+
|
20
|
+
PREDICATES.each do |pred|
|
21
|
+
define_method pred do |other|
|
22
|
+
[pred, self, other]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Table
|
28
|
+
def [](name)
|
29
|
+
Column.new(self, name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup
|
34
|
+
@owner = stub
|
35
|
+
@table = Table.new
|
36
|
+
@subject = ActiveRecord::Query::Subject.new(@owner, :foo, @table)
|
37
|
+
@column = @table[:foo]
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_equality
|
41
|
+
@owner.expects(:<<).with(@column.eq(:bar))
|
42
|
+
@subject == :bar
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_inequality
|
46
|
+
@owner.expects(:<<).with(@column.not_eq(:bar))
|
47
|
+
@subject != :bar
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_matches
|
51
|
+
@owner.expects(:<<).with(@column.matches(:bar))
|
52
|
+
@subject =~ :bar
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_does_not_match
|
56
|
+
@owner.expects(:<<).with(@column.does_not_match(:bar))
|
57
|
+
@subject !~ :bar
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_gt
|
61
|
+
@owner.expects(:<<).with(@column.gt(:bar))
|
62
|
+
@subject > :bar
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_lt
|
66
|
+
@owner.expects(:<<).with(@column.lt(:bar))
|
67
|
+
@subject < :bar
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_gteq
|
71
|
+
@owner.expects(:<<).with(@column.gteq(:bar))
|
72
|
+
@subject >= :bar
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_lteq
|
76
|
+
@owner.expects(:<<).with(@column.lteq(:bar))
|
77
|
+
@subject <= :bar
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_in
|
81
|
+
@owner.expects(:<<).with(@column.in(:bar))
|
82
|
+
@subject.in(:bar)
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_not_in
|
86
|
+
@owner.expects(:<<).with(@column.not_in(:bar))
|
87
|
+
@subject.not_in(:bar)
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_method_missing
|
91
|
+
table = stub
|
92
|
+
sub2 = stub
|
93
|
+
|
94
|
+
Arel::Table.expects(:new).with(:foo).returns(table)
|
95
|
+
ActiveRecord::Query::Subject.expects(:new).with(@owner, :bar, table).returns(sub2)
|
96
|
+
|
97
|
+
assert_equal sub2, @subject.bar
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_method_missing_with_args
|
101
|
+
assert_raises(ArgumentError) { @subject.bar(:foo) }
|
102
|
+
assert_raises(ArgumentError) { @subject.bar { :foo } }
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_respond_to
|
106
|
+
assert @subject.respond_to?(:bar)
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class QueryTest < MiniTest::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@table = :table
|
7
|
+
@query = Query.new(@table, :and)
|
8
|
+
class << @query; include ::Mocha::ObjectMethods; end
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_method_missing
|
12
|
+
assert_raises(ArgumentError) { @query.title(:foo) }
|
13
|
+
assert_raises(ArgumentError) { @query.title { :foo } }
|
14
|
+
|
15
|
+
subject = stub
|
16
|
+
Query::Subject.expects(:new).with(@query, :title).returns(subject)
|
17
|
+
assert_equal subject, @query.title
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_respond_to
|
21
|
+
assert @query.respond_to?(:title)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_table
|
25
|
+
assert_equal @table, @query.table
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_push
|
29
|
+
@query << :foo
|
30
|
+
@query << :bar
|
31
|
+
|
32
|
+
assert_equal [:foo, :bar], @query.arel.expr.children
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_inspect
|
36
|
+
assert_equal "#<ActiveRecord::Query table=:table>", @query.inspect
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_and
|
40
|
+
subcontext = stub(:arel => stub)
|
41
|
+
subject = stub
|
42
|
+
Query::And.expects(:new).returns(subcontext)
|
43
|
+
|
44
|
+
@query.and { }
|
45
|
+
assert_equal [subcontext.arel], @query.arel.expr.children
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_or
|
49
|
+
subcontext = stub(:arel => stub)
|
50
|
+
subject = stub
|
51
|
+
Query::Or.expects(:new).returns(subcontext)
|
52
|
+
|
53
|
+
@query.or { }
|
54
|
+
assert_equal [subcontext.arel], @query.arel.expr.children
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
module ActiveRecordQuery
|
4
|
+
class RelationExtensionTest < MiniTest::Unit::TestCase
|
5
|
+
class FakeRelation
|
6
|
+
module Where
|
7
|
+
attr_reader :where_values
|
8
|
+
|
9
|
+
def where(*args)
|
10
|
+
@where_values = args
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
include Where
|
15
|
+
include RelationExtension
|
16
|
+
|
17
|
+
def table
|
18
|
+
:table
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@relation = FakeRelation.new
|
24
|
+
@query = stub(:arel => stub)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_where_with_args
|
28
|
+
@relation.where :foo
|
29
|
+
assert_equal [:foo], @relation.where_values
|
30
|
+
|
31
|
+
@relation.where(:foo) { :bar }
|
32
|
+
assert_equal [:foo], @relation.where_values
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_where_with_block
|
36
|
+
ActiveRecord::Query.expects(:new).with(:table, :and).returns(@query)
|
37
|
+
|
38
|
+
query = nil
|
39
|
+
@relation.where { |q| query = q }
|
40
|
+
|
41
|
+
assert_equal @query, query
|
42
|
+
assert_equal [@query.arel], @relation.where_values
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_any
|
46
|
+
ActiveRecord::Query.expects(:new).with(:table, :or).returns(@query)
|
47
|
+
|
48
|
+
query = nil
|
49
|
+
@relation.any { |q| query = q }
|
50
|
+
|
51
|
+
assert_equal @query, query
|
52
|
+
assert_equal [@query.arel], @relation.where_values
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record_query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Leighton
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &15665600 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *15665600
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: minitest
|
27
|
+
requirement: &15698260 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *15698260
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: mocha
|
38
|
+
requirement: &15719140 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *15719140
|
47
|
+
description: Proof of concept for proposed new Active Record query API
|
48
|
+
email:
|
49
|
+
- j@jonathanleighton.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- active_record_query.gemspec
|
59
|
+
- lib/active_record/query.rb
|
60
|
+
- lib/active_record/query/context.rb
|
61
|
+
- lib/active_record/query/subject.rb
|
62
|
+
- lib/active_record_query.rb
|
63
|
+
- lib/active_record_query/relation_extension.rb
|
64
|
+
- lib/active_record_query/version.rb
|
65
|
+
- test/helper.rb
|
66
|
+
- test/integration/query_test.rb
|
67
|
+
- test/unit/query/subject_test.rb
|
68
|
+
- test/unit/query_test.rb
|
69
|
+
- test/unit/relation_extension_test.rb
|
70
|
+
homepage: https://github.com/jonleighton/active_record_query
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project: ar_query
|
90
|
+
rubygems_version: 1.8.15
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Proof of concept for proposed new Active Record query API
|
94
|
+
test_files:
|
95
|
+
- test/helper.rb
|
96
|
+
- test/integration/query_test.rb
|
97
|
+
- test/unit/query/subject_test.rb
|
98
|
+
- test/unit/query_test.rb
|
99
|
+
- test/unit/relation_extension_test.rb
|
100
|
+
has_rdoc:
|