squeel 0.5.0 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/README.rdoc +115 -39
  2. data/lib/squeel/adapters/active_record.rb +22 -5
  3. data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
  4. data/lib/squeel/adapters/active_record/3.0/compat.rb +143 -0
  5. data/lib/squeel/adapters/active_record/3.0/context.rb +67 -0
  6. data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
  7. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
  8. data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
  9. data/lib/squeel/adapters/active_record/context.rb +67 -0
  10. data/lib/squeel/adapters/active_record/join_association.rb +10 -56
  11. data/lib/squeel/adapters/active_record/join_dependency.rb +22 -7
  12. data/lib/squeel/adapters/active_record/preloader.rb +21 -0
  13. data/lib/squeel/adapters/active_record/relation.rb +84 -38
  14. data/lib/squeel/context.rb +38 -0
  15. data/lib/squeel/dsl.rb +1 -1
  16. data/lib/squeel/nodes/join.rb +18 -0
  17. data/lib/squeel/nodes/key_path.rb +2 -2
  18. data/lib/squeel/nodes/stub.rb +5 -1
  19. data/lib/squeel/version.rb +1 -1
  20. data/lib/squeel/visitors.rb +2 -2
  21. data/lib/squeel/visitors/{order_visitor.rb → attribute_visitor.rb} +1 -2
  22. data/lib/squeel/visitors/predicate_visitor.rb +13 -11
  23. data/lib/squeel/visitors/symbol_visitor.rb +48 -0
  24. data/spec/helpers/squeel_helper.rb +17 -1
  25. data/spec/spec_helper.rb +31 -0
  26. data/spec/squeel/adapters/active_record/context_spec.rb +50 -0
  27. data/spec/squeel/adapters/active_record/join_association_spec.rb +1 -1
  28. data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +1 -1
  29. data/spec/squeel/adapters/active_record/relation_spec.rb +166 -25
  30. data/spec/squeel/dsl_spec.rb +6 -6
  31. data/spec/squeel/nodes/join_spec.rb +16 -3
  32. data/spec/squeel/nodes/stub_spec.rb +12 -0
  33. data/spec/squeel/visitors/{order_visitor_spec.rb → attribute_visitor_spec.rb} +4 -5
  34. data/spec/squeel/visitors/predicate_visitor_spec.rb +18 -6
  35. data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
  36. data/squeel.gemspec +2 -2
  37. metadata +21 -13
  38. data/lib/squeel/contexts/join_dependency_context.rb +0 -74
  39. data/lib/squeel/visitors/select_visitor.rb +0 -103
  40. data/spec/squeel/contexts/join_dependency_context_spec.rb +0 -43
  41. 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