squeel 1.0.18 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -7
- data/CHANGELOG.md +9 -0
- data/Gemfile +2 -2
- data/README.md +223 -147
- data/lib/generators/templates/squeel.rb +1 -1
- data/lib/squeel/adapters/active_record.rb +1 -1
- data/lib/squeel/adapters/active_record/4.0/compat.rb +17 -0
- data/lib/squeel/adapters/active_record/4.0/context.rb +1 -0
- data/lib/squeel/adapters/active_record/4.0/preloader_extensions.rb +1 -0
- data/lib/squeel/adapters/active_record/4.0/relation_extensions.rb +126 -0
- data/lib/squeel/adapters/active_record/base_extensions.rb +1 -1
- data/lib/squeel/adapters/active_record/context.rb +10 -10
- data/lib/squeel/adapters/active_record/relation_extensions.rb +24 -16
- data/lib/squeel/configuration.rb +1 -0
- data/lib/squeel/constants.rb +2 -2
- data/lib/squeel/context.rb +2 -2
- data/lib/squeel/dsl.rb +1 -1
- data/lib/squeel/nodes.rb +2 -0
- data/lib/squeel/nodes/binary.rb +1 -1
- data/lib/squeel/nodes/function.rb +5 -5
- data/lib/squeel/nodes/join.rb +2 -2
- data/lib/squeel/nodes/key_path.rb +10 -5
- data/lib/squeel/nodes/literal.rb +1 -1
- data/lib/squeel/nodes/nary.rb +5 -7
- data/lib/squeel/nodes/node.rb +6 -0
- data/lib/squeel/nodes/operation.rb +1 -1
- data/lib/squeel/nodes/order.rb +1 -1
- data/lib/squeel/nodes/predicate.rb +5 -5
- data/lib/squeel/nodes/predicate_methods.rb +11 -2
- data/lib/squeel/nodes/sifter.rb +2 -2
- data/lib/squeel/nodes/stub.rb +2 -2
- data/lib/squeel/nodes/unary.rb +1 -1
- data/lib/squeel/version.rb +1 -1
- data/lib/squeel/visitors/predicate_visitation.rb +6 -6
- data/lib/squeel/visitors/predicate_visitor.rb +1 -1
- data/lib/squeel/visitors/visitor.rb +20 -20
- data/spec/spec_helper.rb +6 -4
- data/spec/squeel/adapters/active_record/base_extensions_spec.rb +6 -6
- data/spec/squeel/adapters/active_record/relation_extensions_spec.rb +55 -24
- data/spec/squeel/core_ext/symbol_spec.rb +2 -2
- data/spec/squeel/nodes/key_path_spec.rb +3 -3
- data/spec/squeel/nodes/predicate_operators_spec.rb +4 -4
- data/spec/squeel/visitors/predicate_visitor_spec.rb +11 -11
- data/spec/squeel/visitors/visitor_spec.rb +9 -9
- data/spec/support/models.rb +25 -7
- data/spec/support/schema.rb +1 -1
- data/squeel.gemspec +4 -4
- metadata +19 -12
@@ -23,7 +23,7 @@ describe Symbol do
|
|
23
23
|
it 'creates a function node' do
|
24
24
|
function = :blah.func('foo')
|
25
25
|
function.should be_a Squeel::Nodes::Function
|
26
|
-
function.
|
26
|
+
function.function_name.should eq :blah
|
27
27
|
function.args.should eq ['foo']
|
28
28
|
end
|
29
29
|
end
|
@@ -55,4 +55,4 @@ describe Symbol do
|
|
55
55
|
join._klass.should eq Person
|
56
56
|
end
|
57
57
|
end
|
58
|
-
end
|
58
|
+
end
|
@@ -46,7 +46,7 @@ module Squeel
|
|
46
46
|
it 'creates a named function at its endpoint' do
|
47
47
|
@k.third.fourth.fifth.max(1,2,3)
|
48
48
|
@k.endpoint.should be_a Function
|
49
|
-
@k.endpoint.
|
49
|
+
@k.endpoint.function_name.should eq :max
|
50
50
|
@k.endpoint.args.should eq [1,2,3]
|
51
51
|
end
|
52
52
|
|
@@ -77,7 +77,7 @@ module Squeel
|
|
77
77
|
it 'creates Or nodes with | if the endpoint responds to |' do
|
78
78
|
node = @k.third.fourth.eq('Bob') | Stub.new(:attr).eq('Joe')
|
79
79
|
node.should be_a Or
|
80
|
-
node.left.should
|
80
|
+
node.left.should eql @k
|
81
81
|
node.right.should eql Stub.new(:attr).eq('Joe')
|
82
82
|
end
|
83
83
|
|
@@ -99,7 +99,7 @@ module Squeel
|
|
99
99
|
it 'creates NOT nodes with -@ if the endpoint responds to -@' do
|
100
100
|
node = - @k.third.fourth.eq('Bob')
|
101
101
|
node.should be_a Not
|
102
|
-
node.expr.should
|
102
|
+
node.expr.should eql @k
|
103
103
|
end
|
104
104
|
|
105
105
|
it 'raises NoMethodError with -@ if the endpoint does not respond to -@' do
|
@@ -21,13 +21,13 @@ module Squeel
|
|
21
21
|
n.children.should eq [left, right]
|
22
22
|
end
|
23
23
|
|
24
|
-
it '
|
24
|
+
it 'creates And nodes by appending a new child' do
|
25
25
|
left = :name.matches % 'J%' & :name.matches % '%e'
|
26
26
|
right = :id.gt % 0
|
27
27
|
expected = left.children + [right]
|
28
|
-
left & right
|
29
|
-
|
30
|
-
|
28
|
+
new_and = left & right
|
29
|
+
new_and.should be_a And
|
30
|
+
new_and.children.should eq expected
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -327,7 +327,7 @@ module Squeel
|
|
327
327
|
predicate.to_sql.should be_like '"people"."id" IN (SELECT "people"."id" FROM "people" ORDER BY "people"."id" DESC LIMIT 3)'
|
328
328
|
end
|
329
329
|
|
330
|
-
it 'converts ActiveRecord::Relation values in function arguments to their
|
330
|
+
it 'converts ActiveRecord::Relation values in function arguments to their Arel AST' do
|
331
331
|
predicate = @v.accept(dsl{exists(Person.where{name == 'Aric Smith'})})
|
332
332
|
predicate.should be_a Arel::Nodes::NamedFunction
|
333
333
|
predicate.expressions.first.should be_a Arel::Nodes::SelectStatement
|
@@ -386,7 +386,7 @@ module Squeel
|
|
386
386
|
predicate.right.should eq 'Joe'
|
387
387
|
end
|
388
388
|
|
389
|
-
it 'creates an
|
389
|
+
it 'creates an Arel Grouping node containing an Or node for Or nodes' do
|
390
390
|
left = :name.matches % 'Joe%'
|
391
391
|
right = :id.gt % 1
|
392
392
|
predicate = @v.accept(left | right)
|
@@ -396,23 +396,23 @@ module Squeel
|
|
396
396
|
predicate.expr.right.should be_a Arel::Nodes::GreaterThan
|
397
397
|
end
|
398
398
|
|
399
|
-
it 'creates an
|
399
|
+
it 'creates an Arel Not node for a Not node' do
|
400
400
|
expr = -(:name.matches % 'Joe%')
|
401
401
|
predicate = @v.accept(expr)
|
402
402
|
predicate.should be_a Arel::Nodes::Not
|
403
403
|
end
|
404
404
|
|
405
|
-
it 'creates an
|
405
|
+
it 'creates an Arel NamedFunction node for a Function node' do
|
406
406
|
function = @v.accept(:find_in_set.func())
|
407
407
|
function.should be_a Arel::Nodes::NamedFunction
|
408
408
|
end
|
409
409
|
|
410
|
-
it 'maps symbols in Function args to
|
410
|
+
it 'maps symbols in Function args to Arel attributes' do
|
411
411
|
function = @v.accept(:find_in_set.func(:id, '1,2,3'))
|
412
412
|
function.to_sql.should match /"people"."id"/
|
413
413
|
end
|
414
414
|
|
415
|
-
it 'sets the alias on the
|
415
|
+
it 'sets the alias on the Arel NamedFunction from the Function alias' do
|
416
416
|
function = @v.accept(:find_in_set.func(:id, '1,2,3').as('newname'))
|
417
417
|
function.to_sql.should match /newname/
|
418
418
|
end
|
@@ -427,27 +427,27 @@ module Squeel
|
|
427
427
|
as.to_sql.should match /"people"."name" AS other_name/
|
428
428
|
end
|
429
429
|
|
430
|
-
it 'creates an
|
430
|
+
it 'creates an Arel Addition node for an Operation node with + as operator' do
|
431
431
|
operation = @v.accept(dsl{id + 1})
|
432
432
|
operation.should be_a Arel::Nodes::Addition
|
433
433
|
end
|
434
434
|
|
435
|
-
it 'creates an
|
435
|
+
it 'creates an Arel Subtraction node for an Operation node with - as operator' do
|
436
436
|
operation = @v.accept(dsl{id - 1})
|
437
437
|
operation.should be_a Arel::Nodes::Subtraction
|
438
438
|
end
|
439
439
|
|
440
|
-
it 'creates an
|
440
|
+
it 'creates an Arel Multiplication node for an Operation node with * as operator' do
|
441
441
|
operation = @v.accept(dsl{id * 1})
|
442
442
|
operation.should be_a Arel::Nodes::Multiplication
|
443
443
|
end
|
444
444
|
|
445
|
-
it 'creates an
|
445
|
+
it 'creates an Arel Division node for an Operation node with / as operator' do
|
446
446
|
operation = @v.accept(dsl{id / 1})
|
447
447
|
operation.should be_a Arel::Nodes::Division
|
448
448
|
end
|
449
449
|
|
450
|
-
it 'creates an
|
450
|
+
it 'creates an Arel InfixOperation node for an Operation with a custom operator' do
|
451
451
|
operation = @v.accept(dsl{id.op(:blah, 1)})
|
452
452
|
operation.should be_a Arel::Nodes::InfixOperation
|
453
453
|
end
|
@@ -16,7 +16,7 @@ module Squeel
|
|
16
16
|
@v = Visitor.new(@c)
|
17
17
|
end
|
18
18
|
|
19
|
-
it 'creates a bare
|
19
|
+
it 'creates a bare Arel attribute given a symbol with no asc/desc' do
|
20
20
|
attribute = @v.accept(:name)
|
21
21
|
attribute.should be_a Arel::Attribute
|
22
22
|
attribute.name.should eq :name
|
@@ -71,12 +71,12 @@ module Squeel
|
|
71
71
|
node.to_sql.should be_like "(SELECT \"people\".\"id\" FROM \"people\" WHERE \"people\".\"name\" = 'Aric Smith') aric"
|
72
72
|
end
|
73
73
|
|
74
|
-
it 'creates an
|
74
|
+
it 'creates an Arel NamedFunction node for a Function node' do
|
75
75
|
function = @v.accept(:find_in_set.func())
|
76
76
|
function.should be_a Arel::Nodes::NamedFunction
|
77
77
|
end
|
78
78
|
|
79
|
-
it 'maps symbols in Function args to
|
79
|
+
it 'maps symbols in Function args to Arel attributes' do
|
80
80
|
function = @v.accept(:find_in_set.func(:id, '1,2,3'))
|
81
81
|
function.to_sql.should match /find_in_set\("people"."id", '1,2,3'\)/
|
82
82
|
end
|
@@ -86,7 +86,7 @@ module Squeel
|
|
86
86
|
function.to_sql.should match /find_in_set\("children_people_2"."id", '1,2,3'\)/
|
87
87
|
end
|
88
88
|
|
89
|
-
it 'sets the alias on the
|
89
|
+
it 'sets the alias on the Arel NamedFunction from the Function alias' do
|
90
90
|
function = @v.accept(:find_in_set.func(:id, '1,2,3').as('newname'))
|
91
91
|
function.to_sql.should match /newname/
|
92
92
|
end
|
@@ -106,27 +106,27 @@ module Squeel
|
|
106
106
|
as.to_sql.should match /"children_people"."name" AS other_name/
|
107
107
|
end
|
108
108
|
|
109
|
-
it 'creates an
|
109
|
+
it 'creates an Arel Grouping node for a Squeel Grouping node' do
|
110
110
|
grouping = @v.accept(dsl{_(id)})
|
111
111
|
grouping.should be_a Arel::Nodes::Grouping
|
112
112
|
end
|
113
113
|
|
114
|
-
it 'creates an
|
114
|
+
it 'creates an Arel Addition node for an Operation node with + as operator' do
|
115
115
|
operation = @v.accept(dsl{id + 1})
|
116
116
|
operation.should be_a Arel::Nodes::Addition
|
117
117
|
end
|
118
118
|
|
119
|
-
it 'creates an
|
119
|
+
it 'creates an Arel Subtraction node for an Operation node with - as operator' do
|
120
120
|
operation = @v.accept(dsl{id - 1})
|
121
121
|
operation.should be_a Arel::Nodes::Subtraction
|
122
122
|
end
|
123
123
|
|
124
|
-
it 'creates an
|
124
|
+
it 'creates an Arel Multiplication node for an Operation node with * as operator' do
|
125
125
|
operation = @v.accept(dsl{id * 1})
|
126
126
|
operation.should be_a Arel::Nodes::Multiplication
|
127
127
|
end
|
128
128
|
|
129
|
-
it 'creates an
|
129
|
+
it 'creates an Arel Division node for an Operation node with / as operator' do
|
130
130
|
operation = @v.accept(dsl{id / 1})
|
131
131
|
operation.should be_a Arel::Nodes::Division
|
132
132
|
end
|
data/spec/support/models.rb
CHANGED
@@ -2,10 +2,24 @@ class Person < ActiveRecord::Base
|
|
2
2
|
belongs_to :parent, :class_name => 'Person', :foreign_key => :parent_id
|
3
3
|
has_many :children, :class_name => 'Person', :foreign_key => :parent_id
|
4
4
|
has_many :articles
|
5
|
-
has_many :articles_with_condition, :class_name => 'Article', :conditions => {:title => 'Condition'}
|
6
5
|
has_many :comments
|
6
|
+
if ActiveRecord::VERSION::MAJOR == 4
|
7
|
+
has_many :articles_with_condition, lambda { where :title => 'Condition' },
|
8
|
+
:class_name => 'Article'
|
9
|
+
has_many :article_comments_with_first_post,
|
10
|
+
lambda { where :body => 'first post' },
|
11
|
+
:through => :articles, :source => :comments
|
12
|
+
else
|
13
|
+
has_many :articles_with_condition, :conditions => {:title => 'Condition'},
|
14
|
+
:class_name => 'Article'
|
15
|
+
has_many :article_comments_with_first_post,
|
16
|
+
:conditions => { :body => 'first post' },
|
17
|
+
:through => :articles, :source => :comments
|
18
|
+
end
|
7
19
|
has_many :condition_article_comments, :through => :articles_with_condition, :source => :comments
|
8
|
-
|
20
|
+
if ActiveRecord::VERSION::MAJOR == 4
|
21
|
+
else
|
22
|
+
end
|
9
23
|
has_many :authored_article_comments, :through => :articles,
|
10
24
|
:source => :comments
|
11
25
|
has_many :notes, :as => :notable
|
@@ -28,18 +42,22 @@ class Person < ActiveRecord::Base
|
|
28
42
|
end
|
29
43
|
|
30
44
|
class PersonWithNamePrimaryKey < ActiveRecord::Base
|
31
|
-
|
45
|
+
self.primary_key = 'name'
|
32
46
|
# Set this second, because I'm lazy and don't want to populate another table,
|
33
47
|
# and also don't want to clobber the AR connection's primary_key cache.
|
34
|
-
|
48
|
+
self.table_name = 'people'
|
35
49
|
end
|
36
50
|
|
37
51
|
class PersonNamedBill < ActiveRecord::Base
|
38
52
|
self.table_name = 'people'
|
39
53
|
belongs_to :parent, :class_name => 'Person', :foreign_key => :parent_id
|
40
|
-
|
41
|
-
|
42
|
-
|
54
|
+
if ActiveRecord::VERSION::MAJOR > 3 || ActiveRecord::VERSION::MINOR > 0
|
55
|
+
default_scope lambda { where{name == 'Bill'}.order{id} }
|
56
|
+
else # 3.0 doesn't support callables for default_scope
|
57
|
+
default_scope where{name == 'Bill'}.order{id}
|
58
|
+
end
|
59
|
+
scope :highly_compensated, lambda { where {salary > 200000} }
|
60
|
+
scope :ending_with_ill, lambda { where{name =~ '%ill'} }
|
43
61
|
scope :with_salary_equal_to, lambda { |value| where{abs(salary) == value} }
|
44
62
|
end
|
45
63
|
|
data/spec/support/schema.rb
CHANGED
data/squeel.gemspec
CHANGED
@@ -11,16 +11,16 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.homepage = "http://erniemiller.org/projects/squeel"
|
12
12
|
s.summary = %q{Active Record 3, improved.}
|
13
13
|
s.description = %q{
|
14
|
-
Squeel unlocks the power of
|
14
|
+
Squeel unlocks the power of Arel in your Rails 3 application with
|
15
15
|
a handy block-based syntax. You can write subqueries, access named
|
16
16
|
functions provided by your RDBMS, and more, all without writing
|
17
17
|
SQL strings.
|
18
18
|
}
|
19
19
|
s.rubyforge_project = "squeel"
|
20
20
|
|
21
|
-
s.add_dependency 'activerecord', '
|
22
|
-
s.add_dependency 'activesupport', '
|
23
|
-
s.add_dependency 'polyamorous', '~> 0.
|
21
|
+
s.add_dependency 'activerecord', '>= 3.0'
|
22
|
+
s.add_dependency 'activesupport', '>= 3.0'
|
23
|
+
s.add_dependency 'polyamorous', '~> 0.6.0'
|
24
24
|
s.add_development_dependency 'rspec', '~> 2.6.0'
|
25
25
|
s.add_development_dependency 'machinist', '~> 1.0.6'
|
26
26
|
s.add_development_dependency 'faker', '~> 0.9.5'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: squeel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,14 +9,14 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-07-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
|
-
- -
|
19
|
+
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
21
|
version: '3.0'
|
22
22
|
type: :runtime
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
26
26
|
requirements:
|
27
|
-
- -
|
27
|
+
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '3.0'
|
30
30
|
- !ruby/object:Gem::Dependency
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
33
33
|
none: false
|
34
34
|
requirements:
|
35
|
-
- -
|
35
|
+
- - ! '>='
|
36
36
|
- !ruby/object:Gem::Version
|
37
37
|
version: '3.0'
|
38
38
|
type: :runtime
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
41
41
|
none: false
|
42
42
|
requirements:
|
43
|
-
- -
|
43
|
+
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '3.0'
|
46
46
|
- !ruby/object:Gem::Dependency
|
@@ -50,7 +50,7 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - ~>
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 0.
|
53
|
+
version: 0.6.0
|
54
54
|
type: :runtime
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -58,7 +58,7 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ~>
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.
|
61
|
+
version: 0.6.0
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
63
|
name: rspec
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- - ~>
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: 1.3.3
|
126
|
-
description: ! "\n Squeel unlocks the power of
|
126
|
+
description: ! "\n Squeel unlocks the power of Arel in your Rails 3 application
|
127
127
|
with\n a handy block-based syntax. You can write subqueries, access named\n
|
128
128
|
\ functions provided by your RDBMS, and more, all without writing\n SQL
|
129
129
|
strings.\n "
|
@@ -134,6 +134,8 @@ extensions: []
|
|
134
134
|
extra_rdoc_files: []
|
135
135
|
files:
|
136
136
|
- .gitignore
|
137
|
+
- .ruby-gemset
|
138
|
+
- .ruby-version
|
137
139
|
- .travis.yml
|
138
140
|
- .yardopts
|
139
141
|
- CHANGELOG.md
|
@@ -157,6 +159,10 @@ files:
|
|
157
159
|
- lib/squeel/adapters/active_record/3.2/context.rb
|
158
160
|
- lib/squeel/adapters/active_record/3.2/preloader_extensions.rb
|
159
161
|
- lib/squeel/adapters/active_record/3.2/relation_extensions.rb
|
162
|
+
- lib/squeel/adapters/active_record/4.0/compat.rb
|
163
|
+
- lib/squeel/adapters/active_record/4.0/context.rb
|
164
|
+
- lib/squeel/adapters/active_record/4.0/preloader_extensions.rb
|
165
|
+
- lib/squeel/adapters/active_record/4.0/relation_extensions.rb
|
160
166
|
- lib/squeel/adapters/active_record/base_extensions.rb
|
161
167
|
- lib/squeel/adapters/active_record/compat.rb
|
162
168
|
- lib/squeel/adapters/active_record/context.rb
|
@@ -180,6 +186,7 @@ files:
|
|
180
186
|
- lib/squeel/nodes/key_path.rb
|
181
187
|
- lib/squeel/nodes/literal.rb
|
182
188
|
- lib/squeel/nodes/nary.rb
|
189
|
+
- lib/squeel/nodes/node.rb
|
183
190
|
- lib/squeel/nodes/not.rb
|
184
191
|
- lib/squeel/nodes/operation.rb
|
185
192
|
- lib/squeel/nodes/operators.rb
|
@@ -255,7 +262,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
255
262
|
version: '0'
|
256
263
|
segments:
|
257
264
|
- 0
|
258
|
-
hash:
|
265
|
+
hash: 3013172818139711392
|
259
266
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
260
267
|
none: false
|
261
268
|
requirements:
|
@@ -264,10 +271,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
264
271
|
version: '0'
|
265
272
|
segments:
|
266
273
|
- 0
|
267
|
-
hash:
|
274
|
+
hash: 3013172818139711392
|
268
275
|
requirements: []
|
269
276
|
rubyforge_project: squeel
|
270
|
-
rubygems_version: 1.8.
|
277
|
+
rubygems_version: 1.8.25
|
271
278
|
signing_key:
|
272
279
|
specification_version: 3
|
273
280
|
summary: Active Record 3, improved.
|