squeel 0.5.0 → 0.5.5

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.
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
@@ -3,7 +3,6 @@ require 'active_record'
3
3
  module Squeel
4
4
  module Adapters
5
5
  module ActiveRecord
6
-
7
6
  class JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation
8
7
 
9
8
  def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
@@ -25,66 +24,21 @@ module Squeel
25
24
  reflection.options[:polymorphic] = original_polymorphic
26
25
  end
27
26
 
28
- def join_to(relation)
29
- tables = @tables.dup
30
- foreign_table = parent_table
31
-
32
- # The chain starts with the target table, but we want to end with it here (makes
33
- # more sense in this context), so we reverse
34
- chain.reverse.each_with_index do |reflection, i|
35
- table = tables.shift
36
-
37
- case reflection.source_macro
38
- when :belongs_to
39
- key = reflection.association_primary_key
40
- foreign_key = reflection.foreign_key
41
- when :has_and_belongs_to_many
42
- # Join the join table first...
43
- relation.from(join(
44
- table,
45
- table[reflection.foreign_key].
46
- eq(foreign_table[reflection.active_record_primary_key])
47
- ))
48
-
49
- foreign_table, table = table, tables.shift
50
-
51
- key = reflection.association_primary_key
52
- foreign_key = reflection.association_foreign_key
53
- else
54
- key = reflection.foreign_key
55
- foreign_key = reflection.active_record_primary_key
56
- end
57
-
58
- constraint = table[key].eq(foreign_table[foreign_key])
59
-
60
- if reflection.options[:polymorphic]
61
- constraint = constraint.and(
62
- foreign_table[reflection.foreign_type].eq(reflection.klass.name)
63
- )
64
- end
65
-
66
- if reflection.klass.finder_needs_type_condition?
67
- constraint = table.create_and([
68
- constraint,
69
- reflection.klass.send(:type_condition, table)
70
- ])
71
- end
72
-
73
- relation.from(join(table, constraint))
74
-
75
- unless conditions[i].empty?
76
- relation.where(sanitize(conditions[i], table))
77
- end
27
+ def ==(other)
28
+ super && active_record == other.active_record
29
+ end
78
30
 
79
- # The current table in this iteration becomes the foreign table in the next
80
- foreign_table = table
31
+ def build_constraint(reflection, table, key, foreign_table, foreign_key)
32
+ if reflection.options[:polymorphic]
33
+ super.and(
34
+ foreign_table[reflection.foreign_type].eq(reflection.klass.name)
35
+ )
36
+ else
37
+ super
81
38
  end
82
-
83
- relation
84
39
  end
85
40
 
86
41
  end
87
-
88
42
  end
89
43
  end
90
44
  end
@@ -12,9 +12,24 @@ module Squeel
12
12
  def self.included(base)
13
13
  base.class_eval do
14
14
  alias_method_chain :build, :squeel
15
+ alias_method_chain :graft, :squeel
15
16
  end
16
17
  end
17
18
 
19
+ def graft_with_squeel(*associations)
20
+ associations.each do |association|
21
+ unless join_associations.detect {|a| association == a}
22
+ if association.reflection.options[:polymorphic]
23
+ build(Nodes::Join.new(association.reflection.name, association.join_type, association.reflection.klass),
24
+ association.find_parent_in(self) || join_base, association.join_type)
25
+ else
26
+ build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
27
+ end
28
+ end
29
+ end
30
+ self
31
+ end
32
+
18
33
  def build_with_squeel(associations, parent = nil, join_type = Arel::InnerJoin)
19
34
  associations = associations.symbol if Nodes::Stub === associations
20
35
 
@@ -24,9 +39,9 @@ module Squeel
24
39
  reflection = parent.reflections[associations.name] or
25
40
  raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
26
41
 
27
- unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations)
42
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations.klass)
28
43
  @reflections << reflection
29
- join_association = build_join_association_respecting_polymorphism(reflection, parent, associations)
44
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, associations.klass)
30
45
  join_association.join_type = associations.type
31
46
  @join_parts << join_association
32
47
  cache_joined_association(join_association)
@@ -44,19 +59,19 @@ module Squeel
44
59
  end
45
60
  end
46
61
 
47
- def find_join_association_respecting_polymorphism(reflection, parent, join)
62
+ def find_join_association_respecting_polymorphism(reflection, parent, klass)
48
63
  if association = find_join_association(reflection, parent)
49
64
  unless reflection.options[:polymorphic]
50
65
  association
51
66
  else
52
- association if association.active_record == join.klass
67
+ association if association.active_record == klass
53
68
  end
54
69
  end
55
70
  end
56
71
 
57
- def build_join_association_respecting_polymorphism(reflection, parent, join)
58
- if reflection.options[:polymorphic] && join.polymorphic?
59
- JoinAssociation.new(reflection, self, parent, join.klass)
72
+ def build_join_association_respecting_polymorphism(reflection, parent, klass)
73
+ if reflection.options[:polymorphic] && klass
74
+ JoinAssociation.new(reflection, self, parent, klass)
60
75
  else
61
76
  JoinAssociation.new(reflection, self, parent)
62
77
  end
@@ -0,0 +1,21 @@
1
+ module Squeel
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Preloader
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ alias_method_chain :run, :squeel
9
+ end
10
+ end
11
+
12
+ def run_with_squeel
13
+ unless records.empty?
14
+ Visitors::SymbolVisitor.new.accept(associations).each { |association| preload(association) }
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -11,28 +11,35 @@ module Squeel
11
11
  attr_writer :join_dependency
12
12
  private :join_dependency=
13
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!
14
19
  def join_dependency
15
20
  @join_dependency ||= (build_join_dependency(table.from(table), @joins_values) && @join_dependency)
16
21
  end
17
22
 
18
- def select_visitor
19
- Visitors::SelectVisitor.new(
20
- Contexts::JoinDependencyContext.new(join_dependency)
21
- )
22
- end
23
-
24
23
  def predicate_visitor
25
24
  Visitors::PredicateVisitor.new(
26
- Contexts::JoinDependencyContext.new(join_dependency)
25
+ Context.new(join_dependency)
27
26
  )
28
27
  end
29
28
 
30
- def order_visitor
31
- Visitors::OrderVisitor.new(
32
- Contexts::JoinDependencyContext.new(join_dependency)
29
+ def attribute_visitor
30
+ Visitors::AttributeVisitor.new(
31
+ Context.new(join_dependency)
33
32
  )
34
33
  end
35
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. :(
36
43
  def merge(r, association_name = nil)
37
44
  if association_name || relation_with_different_base?(r)
38
45
  r = r.clone
@@ -66,6 +73,7 @@ module Squeel
66
73
  build_join_dependency(arel, @joins_values) unless @joins_values.empty?
67
74
 
68
75
  predicate_viz = predicate_visitor
76
+ attribute_viz = attribute_visitor
69
77
 
70
78
  collapse_wheres(arel, predicate_viz.accept((@where_values - ['']).uniq))
71
79
 
@@ -74,14 +82,12 @@ module Squeel
74
82
  arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
75
83
  arel.skip(@offset_value) if @offset_value
76
84
 
77
- arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
85
+ arel.group(*attribute_viz.accept(@group_values.uniq.reject{|g| g.blank?})) unless @group_values.empty?
78
86
 
79
- unless @order_values.empty?
80
- order_viz = order_visitor
81
- arel.order(*order_viz.accept(@order_values.uniq.reject{|o| o.blank?}))
82
- end
87
+ order = @reorder_value ? @reorder_value : @order_values
88
+ arel.order(*attribute_viz.accept(order.uniq.reject{|o| o.blank?})) unless order.empty?
83
89
 
84
- build_select(arel, select_visitor.accept(@select_values.uniq))
90
+ build_select(arel, attribute_viz.accept(@select_values.uniq))
85
91
 
86
92
  arel.from(@from_value) if @from_value
87
93
  arel.lock(@lock_value) if @lock_value
@@ -139,13 +145,37 @@ module Squeel
139
145
  manager
140
146
  end
141
147
 
148
+ def includes(*args)
149
+ if block_given? && args.empty?
150
+ super(DSL.eval &Proc.new)
151
+ else
152
+ super
153
+ end
154
+ end
155
+
156
+ def preload(*args)
157
+ if block_given? && args.empty?
158
+ super(DSL.eval &Proc.new)
159
+ else
160
+ super
161
+ end
162
+ end
163
+
164
+ def eager_load(*args)
165
+ if block_given? && args.empty?
166
+ super(DSL.eval &Proc.new)
167
+ else
168
+ super
169
+ end
170
+ end
171
+
142
172
  def select(value = Proc.new)
143
173
  if block_given? && Proc === value
144
174
  if value.arity > 0
145
175
  to_a.select {|*block_args| value.call(*block_args)}
146
176
  else
147
177
  relation = clone
148
- relation.select_values += Array.wrap(DSL.evaluate &value)
178
+ relation.select_values += Array.wrap(DSL.eval &value)
149
179
  relation
150
180
  end
151
181
  else
@@ -153,35 +183,25 @@ module Squeel
153
183
  end
154
184
  end
155
185
 
156
- def where(opts = Proc.new, *rest)
157
- if block_given? && Proc === opts
158
- super(DSL.evaluate &opts)
186
+ def group(*args)
187
+ if block_given? && args.empty?
188
+ super(DSL.eval &Proc.new)
159
189
  else
160
190
  super
161
191
  end
162
192
  end
163
193
 
164
- def build_where(opts, other = [])
165
- case opts
166
- when String, Array
194
+ def order(*args)
195
+ if block_given? && args.empty?
196
+ super(DSL.eval &Proc.new)
197
+ else
167
198
  super
168
- else # Let's prevent PredicateBuilder from doing its thing
169
- [opts, *other].map do |arg|
170
- case arg
171
- when Array # Just in case there's an array in there somewhere
172
- @klass.send(:sanitize_sql, arg)
173
- when Hash
174
- @klass.send(:expand_hash_conditions_for_aggregates, arg)
175
- else
176
- arg
177
- end
178
- end
179
199
  end
180
200
  end
181
201
 
182
- def order(*args)
202
+ def reorder(*args)
183
203
  if block_given? && args.empty?
184
- super(DSL.evaluate &Proc.new)
204
+ super(DSL.eval &Proc.new)
185
205
  else
186
206
  super
187
207
  end
@@ -189,7 +209,15 @@ module Squeel
189
209
 
190
210
  def joins(*args)
191
211
  if block_given? && args.empty?
192
- super(DSL.evaluate &Proc.new)
212
+ super(DSL.eval &Proc.new)
213
+ else
214
+ super
215
+ end
216
+ end
217
+
218
+ def where(opts = Proc.new, *rest)
219
+ if block_given? && Proc === opts
220
+ super(DSL.eval &opts)
193
221
  else
194
222
  super
195
223
  end
@@ -197,12 +225,30 @@ module Squeel
197
225
 
198
226
  def having(*args)
199
227
  if block_given? && args.empty?
200
- super(DSL.evaluate &Proc.new)
228
+ super(DSL.eval &Proc.new)
201
229
  else
202
230
  super
203
231
  end
204
232
  end
205
233
 
234
+ def build_where(opts, other = [])
235
+ case opts
236
+ when String, Array
237
+ super
238
+ else # Let's prevent PredicateBuilder from doing its thing
239
+ [opts, *other].map do |arg|
240
+ case arg
241
+ when Array # Just in case there's an array in there somewhere
242
+ @klass.send(:sanitize_sql, arg)
243
+ when Hash
244
+ @klass.send(:expand_hash_conditions_for_aggregates, arg)
245
+ else
246
+ arg
247
+ end
248
+ end
249
+ end
250
+ end
251
+
206
252
  def collapse_wheres(arel, wheres)
207
253
  wheres = [wheres] unless Array === wheres
208
254
  binaries = wheres.grep(Arel::Nodes::Binary)
@@ -0,0 +1,38 @@
1
+ require 'arel'
2
+
3
+ module Squeel
4
+ class Context
5
+ attr_reader :base, :engine, :arel_visitor
6
+
7
+ def initialize(object)
8
+ @object = object
9
+ @engine = @base.arel_engine
10
+ @arel_visitor = Arel::Visitors.visitor_for @engine
11
+ @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
12
+ @tables = Hash.new {|hash, key| hash[key] = get_table(key)}
13
+ end
14
+
15
+ def find(object, parent = @base)
16
+ raise NotImplementedError, "Subclasses must implement public method find"
17
+ end
18
+
19
+ def traverse(keypath, parent = @base, include_endpoint = false)
20
+ raise NotImplementedError, "Subclasses must implement public method traverse"
21
+ end
22
+
23
+ def contextualize(object)
24
+ @tables[object]
25
+ end
26
+
27
+ def sanitize_sql(conditions, parent)
28
+ raise NotImplementedError, "Subclasses must implement public method sanitize_sql"
29
+ end
30
+
31
+ private
32
+
33
+ def get_table(object)
34
+ raise NotImplementedError, "Subclasses must implement private method get_table"
35
+ end
36
+
37
+ end
38
+ end
@@ -9,7 +9,7 @@ module Squeel
9
9
  end
10
10
  end
11
11
 
12
- def self.evaluate(&block)
12
+ def self.eval(&block)
13
13
  if block.arity > 0
14
14
  yield self.new
15
15
  else
@@ -26,6 +26,24 @@ module Squeel
26
26
  @klass
27
27
  end
28
28
 
29
+ def eql?(other)
30
+ self.class == other.class &&
31
+ self.name == other.name &&
32
+ self.type == other.type &&
33
+ self.klass == other.klass
34
+ end
35
+
36
+ alias :== :eql?
37
+
38
+ def method_missing(method_id, *args)
39
+ super if method_id == :to_ary
40
+ if (args.size == 1) && (Class === args[0])
41
+ KeyPath.new(self, Join.new(method_id, Arel::InnerJoin, args[0]))
42
+ else
43
+ KeyPath.new(self, method_id)
44
+ end
45
+ end
46
+
29
47
  # expand_hash_conditions_for_aggregates assumes our hash keys can be
30
48
  # converted to symbols, so this has to be implemented, but it doesn't
31
49
  # really have to do anything useful.
@@ -101,8 +101,8 @@ module Squeel
101
101
  if endpoint.respond_to? method_id
102
102
  @endpoint = @endpoint.send(method_id, *args)
103
103
  self
104
- elsif Stub === endpoint
105
- @path << endpoint.symbol
104
+ elsif Stub === endpoint || Join === endpoint
105
+ @path << endpoint
106
106
  if args.empty?
107
107
  @endpoint = Stub.new(method_id)
108
108
  elsif (args.size == 1) && (Class === args[0])