active_record_query 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/.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:
|