squeel 0.5.0 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +115 -39
- data/lib/squeel/adapters/active_record.rb +22 -5
- data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
- data/lib/squeel/adapters/active_record/3.0/compat.rb +143 -0
- data/lib/squeel/adapters/active_record/3.0/context.rb +67 -0
- data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
- data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
- data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
- data/lib/squeel/adapters/active_record/context.rb +67 -0
- data/lib/squeel/adapters/active_record/join_association.rb +10 -56
- data/lib/squeel/adapters/active_record/join_dependency.rb +22 -7
- data/lib/squeel/adapters/active_record/preloader.rb +21 -0
- data/lib/squeel/adapters/active_record/relation.rb +84 -38
- data/lib/squeel/context.rb +38 -0
- data/lib/squeel/dsl.rb +1 -1
- data/lib/squeel/nodes/join.rb +18 -0
- data/lib/squeel/nodes/key_path.rb +2 -2
- data/lib/squeel/nodes/stub.rb +5 -1
- data/lib/squeel/version.rb +1 -1
- data/lib/squeel/visitors.rb +2 -2
- data/lib/squeel/visitors/{order_visitor.rb → attribute_visitor.rb} +1 -2
- data/lib/squeel/visitors/predicate_visitor.rb +13 -11
- data/lib/squeel/visitors/symbol_visitor.rb +48 -0
- data/spec/helpers/squeel_helper.rb +17 -1
- data/spec/spec_helper.rb +31 -0
- data/spec/squeel/adapters/active_record/context_spec.rb +50 -0
- data/spec/squeel/adapters/active_record/join_association_spec.rb +1 -1
- data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +1 -1
- data/spec/squeel/adapters/active_record/relation_spec.rb +166 -25
- data/spec/squeel/dsl_spec.rb +6 -6
- data/spec/squeel/nodes/join_spec.rb +16 -3
- data/spec/squeel/nodes/stub_spec.rb +12 -0
- data/spec/squeel/visitors/{order_visitor_spec.rb → attribute_visitor_spec.rb} +4 -5
- data/spec/squeel/visitors/predicate_visitor_spec.rb +18 -6
- data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
- data/squeel.gemspec +2 -2
- metadata +21 -13
- data/lib/squeel/contexts/join_dependency_context.rb +0 -74
- data/lib/squeel/visitors/select_visitor.rb +0 -103
- data/spec/squeel/contexts/join_dependency_context_spec.rb +0 -43
- data/spec/squeel/visitors/select_visitor_spec.rb +0 -115
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
module JoinDependency
|
7
|
+
|
8
|
+
# Yes, I'm using alias_method_chain here. No, I don't feel too
|
9
|
+
# bad about it. JoinDependency, or, to call it by its full proper
|
10
|
+
# name, ::ActiveRecord::Associations::JoinDependency, is one of the
|
11
|
+
# most "for internal use only" chunks of ActiveRecord.
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
alias_method_chain :build, :squeel
|
15
|
+
alias_method_chain :graft, :squeel
|
16
|
+
alias :join_parts :joins
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def graft_with_squeel(*associations)
|
21
|
+
associations.each do |association|
|
22
|
+
unless join_associations.detect {|a| association == a}
|
23
|
+
if association.reflection.options[:polymorphic]
|
24
|
+
build(Nodes::Join.new(association.reflection.name, association.join_type, association.reflection.klass),
|
25
|
+
association.find_parent_in(self) || join_base, association.join_type)
|
26
|
+
else
|
27
|
+
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_with_squeel(associations, parent = nil, join_type = Arel::InnerJoin)
|
35
|
+
associations = associations.symbol if Nodes::Stub === associations
|
36
|
+
|
37
|
+
case associations
|
38
|
+
when Nodes::Join
|
39
|
+
parent ||= @joins.last
|
40
|
+
reflection = parent.reflections[associations.name] or
|
41
|
+
raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
|
42
|
+
|
43
|
+
unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations.klass)
|
44
|
+
@reflections << reflection
|
45
|
+
join_association = build_join_association_respecting_polymorphism(reflection, parent, associations.klass)
|
46
|
+
join_association.join_type = associations.type
|
47
|
+
@joins << join_association
|
48
|
+
cache_joined_association(join_association)
|
49
|
+
end
|
50
|
+
|
51
|
+
join_association
|
52
|
+
when Nodes::KeyPath
|
53
|
+
parent ||= @joins.last
|
54
|
+
associations.path_with_endpoint.each do |key|
|
55
|
+
parent = build(key, parent, join_type)
|
56
|
+
end
|
57
|
+
parent
|
58
|
+
else
|
59
|
+
build_without_squeel(associations, parent, join_type)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_join_association_respecting_polymorphism(reflection, parent, klass)
|
64
|
+
if association = find_join_association(reflection, parent)
|
65
|
+
unless reflection.options[:polymorphic]
|
66
|
+
association
|
67
|
+
else
|
68
|
+
association if association.active_record == klass
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_join_association_respecting_polymorphism(reflection, parent, klass)
|
74
|
+
if reflection.options[:polymorphic] && klass
|
75
|
+
JoinAssociation.new(reflection, self, parent, klass)
|
76
|
+
else
|
77
|
+
JoinAssociation.new(reflection, self, parent)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
module Relation
|
7
|
+
|
8
|
+
JoinAssociation = ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
|
9
|
+
JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
|
10
|
+
|
11
|
+
attr_writer :join_dependency
|
12
|
+
private :join_dependency=
|
13
|
+
|
14
|
+
# Returns a JoinDependency for the current relation.
|
15
|
+
#
|
16
|
+
# We don't need to clear out @join_dependency by overriding #reset, because
|
17
|
+
# the default #reset already does this, despite never setting it anywhere that
|
18
|
+
# I can find. Serendipity, I say!
|
19
|
+
def join_dependency
|
20
|
+
@join_dependency ||= (build_join_dependency(table, @joins_values) && @join_dependency)
|
21
|
+
end
|
22
|
+
|
23
|
+
def predicate_visitor
|
24
|
+
Visitors::PredicateVisitor.new(
|
25
|
+
Context.new(join_dependency)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attribute_visitor
|
30
|
+
Visitors::AttributeVisitor.new(
|
31
|
+
Context.new(join_dependency)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# We need to be able to support merging two relations that have a different
|
36
|
+
# base class. Stock ActiveRecord doesn't have to do anything too special, because
|
37
|
+
# it's already created predicates out of the where_values by now, and they're
|
38
|
+
# already bound to the proper table.
|
39
|
+
#
|
40
|
+
# Squeel, on the other hand, needs to do its best to ensure the predicates are still
|
41
|
+
# winding up against the proper table. Merging relations is a really nifty shortcut
|
42
|
+
# but another little corner of ActiveRecord where the magic quickly fades. :(
|
43
|
+
def merge(r, association_name = nil)
|
44
|
+
if association_name || relation_with_different_base?(r)
|
45
|
+
r = r.clone
|
46
|
+
association_name ||= infer_association_for_relation_merge(r)
|
47
|
+
prepare_relation_for_association_merge!(r, association_name)
|
48
|
+
self.joins_values += [association_name] if reflect_on_association(association_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
super(r)
|
52
|
+
end
|
53
|
+
|
54
|
+
def relation_with_different_base?(r)
|
55
|
+
::ActiveRecord::Relation === r &&
|
56
|
+
base_class.name != r.klass.base_class.name
|
57
|
+
end
|
58
|
+
|
59
|
+
def infer_association_for_relation_merge(r)
|
60
|
+
default_association = reflect_on_all_associations.detect {|a| a.class_name == r.klass.name}
|
61
|
+
default_association ? default_association.name : r.table_name.to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
def prepare_relation_for_association_merge!(r, association_name)
|
65
|
+
r.where_values.map! {|w| Squeel::Visitors::PredicateVisitor.can_accept?(w) ? {association_name => w} : w}
|
66
|
+
r.having_values.map! {|h| Squeel::Visitors::PredicateVisitor.can_accept?(h) ? {association_name => h} : h}
|
67
|
+
r.joins_values.map! {|j| [Symbol, Hash, Nodes::Stub, Nodes::Join].include?(j.class) ? {association_name => j} : j}
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_arel
|
71
|
+
arel = table
|
72
|
+
|
73
|
+
arel = build_join_dependency(arel, @joins_values) unless @joins_values.empty?
|
74
|
+
|
75
|
+
predicate_viz = predicate_visitor
|
76
|
+
attribute_viz = attribute_visitor
|
77
|
+
|
78
|
+
arel = collapse_wheres(arel, predicate_viz.accept((@where_values - ['']).uniq))
|
79
|
+
|
80
|
+
arel = arel.having(*predicate_viz.accept(@having_values.uniq.reject{|h| h.blank?})) unless @having_values.empty?
|
81
|
+
|
82
|
+
arel = arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
|
83
|
+
arel = arel.skip(@offset_value) if @offset_value
|
84
|
+
|
85
|
+
arel = arel.group(*attribute_viz.accept(@group_values.uniq.reject{|g| g.blank?})) unless @group_values.empty?
|
86
|
+
|
87
|
+
arel = arel.order(*attribute_viz.accept(@order_values.uniq.reject{|o| o.blank?})) unless @order_values.empty?
|
88
|
+
|
89
|
+
arel = build_select(arel, attribute_viz.accept(@select_values.uniq))
|
90
|
+
|
91
|
+
arel = arel.from(@from_value) if @from_value
|
92
|
+
arel = arel.lock(@lock_value) if @lock_value
|
93
|
+
|
94
|
+
arel
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_join_dependency(relation, joins)
|
98
|
+
association_joins = []
|
99
|
+
|
100
|
+
joins = joins.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
|
101
|
+
|
102
|
+
joins.each do |join|
|
103
|
+
association_joins << join if [Hash, Array, Symbol, Nodes::Stub, Nodes::Join, Nodes::KeyPath].include?(join.class) && !array_of_strings?(join)
|
104
|
+
end
|
105
|
+
|
106
|
+
stashed_association_joins = joins.grep(::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
|
107
|
+
|
108
|
+
non_association_joins = (joins - association_joins - stashed_association_joins)
|
109
|
+
custom_joins = custom_join_sql(*non_association_joins)
|
110
|
+
|
111
|
+
self.join_dependency = JoinDependency.new(@klass, association_joins, custom_joins)
|
112
|
+
|
113
|
+
join_dependency.graft(*stashed_association_joins)
|
114
|
+
|
115
|
+
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
|
116
|
+
|
117
|
+
to_join = []
|
118
|
+
|
119
|
+
join_dependency.join_associations.each do |association|
|
120
|
+
if (association_relation = association.relation).is_a?(Array)
|
121
|
+
to_join << [association_relation.first, association.join_type, association.association_join.first]
|
122
|
+
to_join << [association_relation.last, association.join_type, association.association_join.last]
|
123
|
+
else
|
124
|
+
to_join << [association_relation, association.join_type, association.association_join]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
to_join.uniq.each do |left, join_type, right|
|
129
|
+
relation = relation.join(left, join_type).on(*right)
|
130
|
+
end
|
131
|
+
|
132
|
+
relation = relation.join(custom_joins)
|
133
|
+
end
|
134
|
+
|
135
|
+
def includes(*args)
|
136
|
+
if block_given? && args.empty?
|
137
|
+
super(DSL.eval &Proc.new)
|
138
|
+
else
|
139
|
+
super
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def preload(*args)
|
144
|
+
if block_given? && args.empty?
|
145
|
+
super(DSL.eval &Proc.new)
|
146
|
+
else
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def eager_load(*args)
|
152
|
+
if block_given? && args.empty?
|
153
|
+
super(DSL.eval &Proc.new)
|
154
|
+
else
|
155
|
+
super
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def select(value = Proc.new)
|
160
|
+
if block_given? && Proc === value
|
161
|
+
if value.arity > 0
|
162
|
+
to_a.select {|*block_args| value.call(*block_args)}
|
163
|
+
else
|
164
|
+
relation = clone
|
165
|
+
relation.select_values += Array.wrap(DSL.eval &value)
|
166
|
+
relation
|
167
|
+
end
|
168
|
+
else
|
169
|
+
super
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def group(*args)
|
174
|
+
if block_given? && args.empty?
|
175
|
+
super(DSL.eval &Proc.new)
|
176
|
+
else
|
177
|
+
super
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def order(*args)
|
182
|
+
if block_given? && args.empty?
|
183
|
+
super(DSL.eval &Proc.new)
|
184
|
+
else
|
185
|
+
super
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def reorder(*args)
|
190
|
+
if block_given? && args.empty?
|
191
|
+
super(DSL.eval &Proc.new)
|
192
|
+
else
|
193
|
+
super
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def joins(*args)
|
198
|
+
if block_given? && args.empty?
|
199
|
+
super(DSL.eval &Proc.new)
|
200
|
+
else
|
201
|
+
super
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def where(opts = Proc.new, *rest)
|
206
|
+
if block_given? && Proc === opts
|
207
|
+
super(DSL.eval &opts)
|
208
|
+
else
|
209
|
+
super
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def having(*args)
|
214
|
+
if block_given? && args.empty?
|
215
|
+
super(DSL.eval &Proc.new)
|
216
|
+
else
|
217
|
+
super
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def build_where(opts, other = [])
|
222
|
+
case opts
|
223
|
+
when String, Array
|
224
|
+
super
|
225
|
+
else # Let's prevent PredicateBuilder from doing its thing
|
226
|
+
[opts, *other].map do |arg|
|
227
|
+
case arg
|
228
|
+
when Array # Just in case there's an array in there somewhere
|
229
|
+
@klass.send(:sanitize_sql, arg)
|
230
|
+
when Hash
|
231
|
+
@klass.send(:expand_hash_conditions_for_aggregates, arg)
|
232
|
+
else
|
233
|
+
arg
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def collapse_wheres(arel, wheres)
|
240
|
+
wheres = [wheres] unless Array === wheres
|
241
|
+
binaries = wheres.grep(Arel::Nodes::Binary)
|
242
|
+
|
243
|
+
groups = binaries.group_by {|b| [b.class, b.left]}
|
244
|
+
|
245
|
+
groups.each do |_, bins|
|
246
|
+
arel = arel.where(bins.inject(&:and))
|
247
|
+
end
|
248
|
+
|
249
|
+
(wheres - binaries).each do |where|
|
250
|
+
where = Arel.sql(where) if String === where
|
251
|
+
arel = arel.where(Arel::Nodes::Grouping.new(where))
|
252
|
+
end
|
253
|
+
|
254
|
+
arel
|
255
|
+
end
|
256
|
+
|
257
|
+
def find_equality_predicates(nodes)
|
258
|
+
nodes.map { |node|
|
259
|
+
case node
|
260
|
+
when Arel::Nodes::Equality
|
261
|
+
node
|
262
|
+
when Arel::Nodes::Grouping
|
263
|
+
find_equality_predicates([node.expr])
|
264
|
+
when Arel::Nodes::And
|
265
|
+
find_equality_predicates(node.children)
|
266
|
+
else
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
}.compact.flatten
|
270
|
+
end
|
271
|
+
|
272
|
+
# Simulate the logic that occurs in #to_a
|
273
|
+
#
|
274
|
+
# This will let us get a dump of the SQL that will be run against the
|
275
|
+
# DB for debug purposes without actually running the query.
|
276
|
+
def debug_sql
|
277
|
+
if eager_loading?
|
278
|
+
including = (@eager_load_values + @includes_values).uniq
|
279
|
+
join_dependency = JoinDependency.new(@klass, including, nil)
|
280
|
+
construct_relation_for_association_find(join_dependency).to_sql
|
281
|
+
else
|
282
|
+
arel.to_sql
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
### ZOMG ALIAS_METHOD_CHAIN IS BELOW. HIDE YOUR EYES!
|
287
|
+
# ...
|
288
|
+
# ...
|
289
|
+
# ...
|
290
|
+
# Since you're still looking, let me explain this horrible
|
291
|
+
# transgression you see before you.
|
292
|
+
# You see, Relation#where_values_hash is defined on the
|
293
|
+
# ActiveRecord::Relation class. Since it's defined there, but
|
294
|
+
# I would very much like to modify its behavior, I have three
|
295
|
+
# choices.
|
296
|
+
#
|
297
|
+
# 1. Inherit from ActiveRecord::Relation in a Squeel::Relation
|
298
|
+
# class, and make an attempt to usurp all of the various calls
|
299
|
+
# to methods on ActiveRecord::Relation by doing some really
|
300
|
+
# evil stuff with constant reassignment, all for the sake of
|
301
|
+
# being able to use super().
|
302
|
+
#
|
303
|
+
# 2. Submit a patch to Rails core, breaking this method off into
|
304
|
+
# another module, all for my own selfish desire to use super()
|
305
|
+
# while mucking about in Rails internals.
|
306
|
+
#
|
307
|
+
# 3. Use alias_method_chain, and say 10 hail Hanssons as penance.
|
308
|
+
#
|
309
|
+
# I opted to go with #3. Except for the hail Hansson thing.
|
310
|
+
# Unless you're DHH, in which case, I totally said them.
|
311
|
+
|
312
|
+
def self.included(base)
|
313
|
+
base.class_eval do
|
314
|
+
alias_method_chain :where_values_hash, :squeel
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def where_values_hash_with_squeel
|
319
|
+
equalities = find_equality_predicates(predicate_visitor.accept(@where_values))
|
320
|
+
|
321
|
+
Hash[equalities.map { |where| [where.left.name, where.right] }]
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'squeel/context'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
class Context < ::Squeel::Context
|
7
|
+
# Because the AR::Associations namespace is insane
|
8
|
+
JoinPart = ::ActiveRecord::Associations::JoinDependency::JoinPart
|
9
|
+
|
10
|
+
def initialize(object)
|
11
|
+
@base = object.join_base
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def find(object, parent = @base)
|
16
|
+
if JoinPart === parent
|
17
|
+
object = object.to_sym if String === object
|
18
|
+
case object
|
19
|
+
when Symbol, Nodes::Stub
|
20
|
+
@object.join_associations.detect { |j|
|
21
|
+
j.reflection.name == object.to_sym && j.parent == parent
|
22
|
+
}
|
23
|
+
when Nodes::Join
|
24
|
+
@object.join_associations.detect { |j|
|
25
|
+
j.reflection.name == object.name && j.parent == parent &&
|
26
|
+
(object.polymorphic? ? j.reflection.klass == object.klass : true)
|
27
|
+
}
|
28
|
+
else
|
29
|
+
@object.join_associations.detect { |j|
|
30
|
+
j.reflection == object && j.parent == parent
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def traverse(keypath, parent = @base, include_endpoint = false)
|
37
|
+
parent = @base if keypath.absolute?
|
38
|
+
keypath.path.each do |key|
|
39
|
+
parent = find(key, parent) || key
|
40
|
+
end
|
41
|
+
parent = find(keypath.endpoint, parent) if include_endpoint
|
42
|
+
|
43
|
+
parent
|
44
|
+
end
|
45
|
+
|
46
|
+
def sanitize_sql(conditions, parent)
|
47
|
+
parent.active_record.send(:sanitize_sql, conditions, parent.aliased_table_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_table(object)
|
53
|
+
if [Symbol, Nodes::Stub].include?(object.class)
|
54
|
+
Arel::Table.new(object.to_sym, :engine => @engine)
|
55
|
+
elsif Nodes::Join === object
|
56
|
+
object.klass ? object.klass.arel_table : Arel::Table.new(object.name, :engine => @engine)
|
57
|
+
elsif object.respond_to?(:aliased_table_name)
|
58
|
+
Arel::Table.new(object.table_name, :as => object.aliased_table_name, :engine => @engine)
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Unable to get table for #{object}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|