squeel 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.yardopts +3 -0
  2. data/Gemfile +8 -3
  3. data/README.md +368 -0
  4. data/lib/core_ext/hash.rb +8 -8
  5. data/lib/core_ext/symbol.rb +7 -6
  6. data/lib/squeel.rb +2 -0
  7. data/lib/squeel/adapters/active_record.rb +25 -20
  8. data/lib/squeel/adapters/active_record/3.0/compat.rb +1 -2
  9. data/lib/squeel/adapters/active_record/3.0/context.rb +6 -7
  10. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +5 -5
  11. data/lib/squeel/adapters/active_record/context.rb +6 -7
  12. data/lib/squeel/adapters/active_record/join_dependency.rb +5 -5
  13. data/lib/squeel/configuration.rb +29 -0
  14. data/lib/squeel/constants.rb +1 -0
  15. data/lib/squeel/context.rb +36 -7
  16. data/lib/squeel/dsl.rb +57 -2
  17. data/lib/squeel/nodes.rb +6 -0
  18. data/lib/squeel/nodes/and.rb +1 -0
  19. data/lib/squeel/nodes/binary.rb +11 -2
  20. data/lib/squeel/nodes/function.rb +30 -48
  21. data/lib/squeel/nodes/join.rb +56 -12
  22. data/lib/squeel/nodes/key_path.rb +68 -2
  23. data/lib/squeel/nodes/nary.rb +12 -2
  24. data/lib/squeel/nodes/not.rb +1 -0
  25. data/lib/squeel/nodes/operation.rb +9 -0
  26. data/lib/squeel/nodes/operators.rb +16 -0
  27. data/lib/squeel/nodes/or.rb +1 -0
  28. data/lib/squeel/nodes/order.rb +19 -1
  29. data/lib/squeel/nodes/predicate.rb +25 -3
  30. data/lib/squeel/nodes/predicate_operators.rb +12 -0
  31. data/lib/squeel/nodes/stub.rb +55 -48
  32. data/lib/squeel/nodes/unary.rb +7 -1
  33. data/lib/squeel/predicate_methods.rb +2 -10
  34. data/lib/squeel/version.rb +1 -1
  35. data/lib/squeel/visitors/attribute_visitor.rb +80 -4
  36. data/lib/squeel/visitors/base.rb +70 -4
  37. data/lib/squeel/visitors/predicate_visitor.rb +28 -9
  38. data/lib/squeel/visitors/symbol_visitor.rb +1 -1
  39. data/spec/core_ext/symbol_spec.rb +2 -2
  40. data/spec/spec_helper.rb +6 -1
  41. data/spec/squeel/adapters/active_record/context_spec.rb +0 -7
  42. data/spec/squeel/adapters/active_record/relation_spec.rb +27 -0
  43. data/spec/squeel/dsl_spec.rb +20 -1
  44. data/spec/squeel/nodes/join_spec.rb +11 -4
  45. data/spec/squeel/nodes/key_path_spec.rb +1 -1
  46. data/spec/squeel/nodes/predicate_spec.rb +0 -42
  47. data/spec/squeel/nodes/stub_spec.rb +9 -8
  48. data/spec/squeel/visitors/predicate_visitor_spec.rb +34 -9
  49. data/squeel.gemspec +6 -9
  50. metadata +8 -10
  51. data/README.rdoc +0 -117
  52. data/lib/squeel/predicate_methods/function.rb +0 -9
  53. data/lib/squeel/predicate_methods/predicate.rb +0 -11
  54. data/lib/squeel/predicate_methods/stub.rb +0 -9
  55. data/lib/squeel/predicate_methods/symbol.rb +0 -9
@@ -3,32 +3,61 @@ require 'squeel/nodes'
3
3
 
4
4
  module Squeel
5
5
  module Visitors
6
+ # The Base visitor class, containing the default behavior common to subclasses.
6
7
  class Base
7
8
  attr_accessor :context
8
- delegate :contextualize, :find, :traverse, :sanitize_sql, :engine, :arel_visitor, :to => :context
9
+ delegate :contextualize, :find, :traverse, :engine, :arel_visitor, :to => :context
9
10
 
11
+ # Create a new Visitor that uses the supplied context object to contextualize
12
+ # visited nodes.
13
+ #
14
+ # @param [Context] context The context to use for node visitation.
10
15
  def initialize(context = nil)
11
16
  @context = context
12
17
  end
13
18
 
19
+ # Accept an object.
20
+ #
21
+ # @param object The object to visit
22
+ # @param parent The parent of this object, to track the object's place in
23
+ # any association hierarchy.
24
+ # @return The results of the node visitation, typically an ARel object of some kind.
14
25
  def accept(object, parent = context.base)
15
26
  visit(object, parent)
16
27
  end
17
28
 
29
+ # @param object The object to check
30
+ # @return [Boolean] Whether or not the visitor can accept the given object
18
31
  def can_accept?(object)
19
- respond_to? DISPATCH[object.class]
32
+ self.class.can_accept? object
20
33
  end
21
34
 
35
+ # @param object The object to check
36
+ # @return [Boolean] Whether or not visitors of this class can accept the given object
22
37
  def self.can_accept?(object)
23
- method_defined? DISPATCH[object.class]
38
+ @can_accept ||= Hash.new do |hash, klass|
39
+ hash[klass] = klass.ancestors.detect { |ancestor|
40
+ private_method_defined? DISPATCH[ancestor]
41
+ } ? true : false
42
+ end
43
+ @can_accept[object.class]
24
44
  end
25
45
 
26
46
  private
27
47
 
48
+ # A hash that caches the method name to use for a visitor for a given class
28
49
  DISPATCH = Hash.new do |hash, klass|
29
- hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
50
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
30
51
  end
31
52
 
53
+ # Important to avoid accidentally allowing the default ARel visitor's
54
+ # last_column quoting behavior (where a value is quoted as though it
55
+ # is of the type of the last visited column). This can wreak havoc with
56
+ # Functions and Operations.
57
+ #
58
+ # @param object The object to check
59
+ # @return [Boolean] Whether or not the ARel visitor will try to quote the object if
60
+ # not passed as an SqlLiteral.
32
61
  def quoted?(object)
33
62
  case object
34
63
  when Arel::Nodes::SqlLiteral, Bignum, Fixnum
@@ -38,9 +67,46 @@ module Squeel
38
67
  end
39
68
  end
40
69
 
70
+ # Quote a value based on its type, not on the last column used by the
71
+ # ARel visitor. This is occasionally necessary to avoid having ARel
72
+ # quote a value according to an integer column, converting 'My String' to 0.
73
+ #
74
+ # @param value The value to quote
75
+ # @return [Arel::Nodes::SqlLiteral] if the value needs to be pre-quoted
76
+ # @return the unquoted value, if default quoting won't hurt.
77
+ def quote(value)
78
+ if quoted? value
79
+ case value
80
+ when Array
81
+ value.map {|v| quote(v)}
82
+ when Range
83
+ Range.new(quote(value.begin), quote(value.end), value.exclude_end?)
84
+ else
85
+ Arel.sql(arel_visitor.accept value)
86
+ end
87
+ else
88
+ value
89
+ end
90
+ end
91
+
92
+ # Visit the object. This is not called directly, but instead via the public
93
+ # #accept method.
94
+ #
95
+ # @param object The object to visit
96
+ # @param parent The object's parent within the context
41
97
  def visit(object, parent)
42
98
  send(DISPATCH[object.class], object, parent)
99
+ rescue NoMethodError => e
100
+ raise e if respond_to?(DISPATCH[object.class], true)
101
+
102
+ superklass = object.class.ancestors.find { |klass|
103
+ respond_to?(DISPATCH[klass], true)
104
+ }
105
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
106
+ DISPATCH[object.class] = DISPATCH[superklass]
107
+ retry
43
108
  end
109
+
44
110
  end
45
111
  end
46
112
  end
@@ -4,6 +4,8 @@ module Squeel
4
4
  module Visitors
5
5
  class PredicateVisitor < Base
6
6
 
7
+ private
8
+
7
9
  def visit_Hash(o, parent)
8
10
  predicates = o.map do |k, v|
9
11
  if implies_context_change?(v)
@@ -26,6 +28,10 @@ module Squeel
26
28
  o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
27
29
  end
28
30
 
31
+ def visit_ActiveRecord_Base(o, parent)
32
+ o.id
33
+ end
34
+
29
35
  def visit_Squeel_Nodes_KeyPath(o, parent)
30
36
  parent = traverse(o, parent)
31
37
 
@@ -43,8 +49,12 @@ module Squeel
43
49
  else
44
50
  value = accept(value, parent) if can_accept?(value)
45
51
  end
46
- if Nodes::Function === o.expr
52
+
53
+ case o.expr
54
+ when Nodes::Stub
47
55
  accept(o.expr, parent).send(o.method_name, value)
56
+ when Nodes::Function
57
+ accept(o.expr, parent).send(o.method_name, quote(value))
48
58
  else
49
59
  contextualize(parent)[o.expr].send(o.method_name, value)
50
60
  end
@@ -60,7 +70,7 @@ module Squeel
60
70
  when Symbol, Nodes::Stub
61
71
  Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
62
72
  else
63
- quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
73
+ quote arg
64
74
  end
65
75
  end
66
76
  Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
@@ -80,7 +90,7 @@ module Squeel
80
90
  when Symbol, Nodes::Stub
81
91
  Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
82
92
  else
83
- quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
93
+ quote arg
84
94
  end
85
95
  end
86
96
 
@@ -117,8 +127,6 @@ module Squeel
117
127
  true
118
128
  when Nodes::KeyPath
119
129
  can_accept?(v.endpoint) && !(Nodes::Stub === v.endpoint)
120
- when Array
121
- (!v.empty? && v.all? {|val| can_accept?(val)})
122
130
  else
123
131
  false
124
132
  end
@@ -156,11 +164,11 @@ module Squeel
156
164
 
157
165
  case k
158
166
  when Nodes::Predicate
159
- accept(k % v, parent)
167
+ accept(k % quote_for_node(k.expr, v), parent)
160
168
  when Nodes::Function
161
- arel_predicate_for(accept(k, parent), v, parent)
169
+ arel_predicate_for(accept(k, parent), quote(v), parent)
162
170
  when Nodes::KeyPath
163
- accept(k % v, parent)
171
+ accept(k % quote_for_node(k.endpoint, v), parent)
164
172
  else
165
173
  attribute = contextualize(parent)[k.to_sym]
166
174
  arel_predicate_for(attribute, v, parent)
@@ -168,14 +176,25 @@ module Squeel
168
176
  end
169
177
 
170
178
  def arel_predicate_for(attribute, value, parent)
179
+ value = can_accept?(value) ? accept(value, parent) : value
171
180
  if [Array, Range, Arel::SelectManager].include?(value.class)
172
181
  attribute.in(value)
173
182
  else
174
- value = can_accept?(value) ? accept(value, parent) : value
175
183
  attribute.eq(value)
176
184
  end
177
185
  end
178
186
 
187
+ def quote_for_node(node, v)
188
+ case node
189
+ when Nodes::Function
190
+ quote(v)
191
+ when Nodes::Predicate
192
+ Nodes::Function === node.expr ? quote(v) : v
193
+ else
194
+ v
195
+ end
196
+ end
197
+
179
198
  end
180
199
  end
181
200
  end
@@ -40,7 +40,7 @@ module Squeel
40
40
  end
41
41
 
42
42
  def visit_Squeel_Nodes_Join(o, parent)
43
- o.name
43
+ o._name
44
44
  end
45
45
 
46
46
  end
@@ -56,13 +56,13 @@ describe Symbol do
56
56
  it 'creates inner joins' do
57
57
  join = :join.inner
58
58
  join.should be_a Squeel::Nodes::Join
59
- join.type.should eq Arel::InnerJoin
59
+ join._type.should eq Arel::InnerJoin
60
60
  end
61
61
 
62
62
  it 'creates outer joins' do
63
63
  join = :join.outer
64
64
  join.should be_a Squeel::Nodes::Join
65
- join.type.should eq Arel::OuterJoin
65
+ join._type.should eq Arel::OuterJoin
66
66
  end
67
67
 
68
68
  end
@@ -40,7 +40,12 @@ Sham.define do
40
40
  end
41
41
 
42
42
  RSpec.configure do |config|
43
- config.before(:suite) { Schema.create }
43
+ config.before(:suite) do
44
+ puts '=' * 80
45
+ puts "Running specs against ActiveRecord #{ActiveRecord::VERSION::STRING} and ARel #{Arel::VERSION}..."
46
+ puts '=' * 80
47
+ Schema.create
48
+ end
44
49
  config.before(:all) { Sham.reset(:before_all) }
45
50
  config.before(:each) { Sham.reset(:before_each) }
46
51
 
@@ -15,13 +15,6 @@ module Squeel
15
15
  @c = Context.new(@jd)
16
16
  end
17
17
 
18
- it 'finds associations' do
19
- last_association = @jd.join_parts.last
20
- next_to_last_association = @jd.join_parts[-2]
21
-
22
- @c.find(:parent, next_to_last_association).should eq last_association
23
- end
24
-
25
18
  it 'contextualizes join parts with the proper alias' do
26
19
  table = @c.contextualize @jd.join_parts.last
27
20
  table.table_alias.should eq 'parents_people_2'
@@ -189,6 +189,33 @@ module Squeel
189
189
 
190
190
  end
191
191
 
192
+ describe '#to_sql' do
193
+ it 'casts a non-acceptable value for a Function key properly in a hash' do
194
+ relation = Person.joins(:children).where(:children => {:coalesce.func(:name, 'Mr. No-name') => 'Ernie'})
195
+ relation.to_sql.should match /'Ernie'/
196
+ end
197
+
198
+ it 'casts a non-acceptable value for a Predicate containing a Function expr properly' do
199
+ relation = Person.joins(:children).where(:children => {:coalesce.func(:name, 'Mr. No-name').eq => 'Ernie'})
200
+ relation.to_sql.should match /'Ernie'/
201
+ end
202
+
203
+ it 'casts a non-acceptable value for a KeyPath with a Function endpoint properly' do
204
+ relation = Person.joins(:children).where{{children.coalesce(:name, 'Mr. No-name') => 'Ernie'}}
205
+ relation.to_sql.should match /'Ernie'/
206
+ end
207
+
208
+ it 'casts a non-acceptable value for a KeyPath with a Predicate endpoint containing a Function expr properly' do
209
+ relation = Person.joins(:children).where{{children.coalesce(:name, 'Mr. No-name').eq => 'Ernie'}}
210
+ relation.to_sql.should match /'Ernie'/
211
+ end
212
+
213
+ it 'casts a non-acceptable value for a Function with a Predicate endpoint containing a Function expr properly' do
214
+ relation = Person.joins(:children).where{children.coalesce(:name, 'Mr. No-name') == 'Ernie'}
215
+ relation.to_sql.should match /'Ernie'/
216
+ end
217
+ end
218
+
192
219
  describe '#includes' do
193
220
 
194
221
  it 'builds options with a block' do
@@ -18,7 +18,7 @@ module Squeel
18
18
  it 'creates polymorphic join nodes when a method has a single class argument' do
19
19
  result = DSL.eval { association(Person) }
20
20
  result.should be_a Nodes::Join
21
- result.klass.should eq Person
21
+ result._klass.should eq Person
22
22
  end
23
23
 
24
24
  it 'handles OR between predicates' do
@@ -69,5 +69,24 @@ module Squeel
69
69
  result.value.should eq 'test'
70
70
  end
71
71
 
72
+ describe '#my' do
73
+ it 'allows access to caller instance variables' do
74
+ @test_var = "test"
75
+ result = DSL.eval{my{@test_var}}
76
+ result.should be_a String
77
+ result.should eq @test_var
78
+ end
79
+
80
+ it 'allows access to caller methods' do
81
+ def test_scoped_method
82
+ :name
83
+ end
84
+
85
+ result = DSL.eval{my{test_scoped_method}}
86
+ result.should be_a Symbol
87
+ result.should eq :name
88
+ end
89
+ end
90
+
72
91
  end
73
92
  end
@@ -9,18 +9,18 @@ module Squeel
9
9
  end
10
10
 
11
11
  it 'defaults to Arel::InnerJoin' do
12
- @j.type.should eq Arel::InnerJoin
12
+ @j._type.should eq Arel::InnerJoin
13
13
  end
14
14
 
15
15
  it 'allows setting join type' do
16
16
  @j.outer
17
- @j.type.should eq Arel::OuterJoin
17
+ @j._type.should eq Arel::OuterJoin
18
18
  end
19
19
 
20
20
  it 'allows setting polymorphic class' do
21
- @j.klass = Person
21
+ @j._klass = Person
22
22
  @j.should be_polymorphic
23
- @j.klass.should eq Person
23
+ @j._klass.should eq Person
24
24
  end
25
25
 
26
26
  it 'creates a KeyPath when sent an unknown method' do
@@ -35,6 +35,13 @@ module Squeel
35
35
  keypath.path_with_endpoint.should eq [@j, Join.new(:another, Arel::InnerJoin, Person)]
36
36
  end
37
37
 
38
+ it 'creates an absolute keypath with just an endpoint with ~' do
39
+ node = ~@j
40
+ node.should be_a KeyPath
41
+ node.path.should eq []
42
+ node.endpoint.should eq @j
43
+ end
44
+
38
45
  end
39
46
  end
40
47
  end
@@ -20,7 +20,7 @@ module Squeel
20
20
 
21
21
  it 'stops appending once its endpoint is not a Stub' do
22
22
  @k.third.fourth.fifth == 'cinco'
23
- @k.endpoint.should eq Predicate.new(:fifth, :eq, 'cinco')
23
+ @k.endpoint.should eq Predicate.new(Stub.new(:fifth), :eq, 'cinco')
24
24
  expect { @k.another }.to raise_error NoMethodError
25
25
  end
26
26
 
@@ -4,48 +4,6 @@ module Squeel
4
4
  module Nodes
5
5
  describe Predicate do
6
6
 
7
- context 'ARel predicate methods' do
8
- before do
9
- @p = Predicate.new(:attribute)
10
- end
11
-
12
- Squeel::Constants::PREDICATES.each do |method_name|
13
- it "creates #{method_name} predicates with no value" do
14
- predicate = @p.send(method_name)
15
- predicate.expr.should eq :attribute
16
- predicate.method_name.should eq method_name
17
- predicate.value?.should be_false
18
- end
19
-
20
- it "creates #{method_name} predicates with a value" do
21
- predicate = @p.send(method_name, 'value')
22
- predicate.expr.should eq :attribute
23
- predicate.method_name.should eq method_name
24
- predicate.value.should eq 'value'
25
- end
26
- end
27
-
28
- Squeel::Constants::PREDICATE_ALIASES.each do |method_name, aliases|
29
- aliases.each do |aliaz|
30
- ['', '_any', '_all'].each do |suffix|
31
- it "creates #{method_name.to_s + suffix} predicates with no value using the alias #{aliaz.to_s + suffix}" do
32
- predicate = @p.send(aliaz.to_s + suffix)
33
- predicate.expr.should eq :attribute
34
- predicate.method_name.should eq "#{method_name}#{suffix}".to_sym
35
- predicate.value?.should be_false
36
- end
37
-
38
- it "creates #{method_name.to_s + suffix} predicates with a value using the alias #{aliaz.to_s + suffix}" do
39
- predicate = @p.send((aliaz.to_s + suffix), 'value')
40
- predicate.expr.should eq :attribute
41
- predicate.method_name.should eq "#{method_name}#{suffix}".to_sym
42
- predicate.value.should eq 'value'
43
- end
44
- end
45
- end
46
- end
47
- end
48
-
49
7
  it 'accepts a value on instantiation' do
50
8
  @p = Predicate.new :name, :eq, 'value'
51
9
  @p.value.should eq 'value'
@@ -39,6 +39,13 @@ module Squeel
39
39
  keypath.path_with_endpoint.should eq [@s, Join.new(:another, Arel::InnerJoin, Person)]
40
40
  end
41
41
 
42
+ it 'creates an absolute keypath with just an endpoint with ~' do
43
+ node = ~@s
44
+ node.should be_a KeyPath
45
+ node.path.should eq []
46
+ node.endpoint.should eq @s
47
+ end
48
+
42
49
  Squeel::Constants::PREDICATES.each do |method_name|
43
50
  it "creates #{method_name} predicates with no value" do
44
51
  predicate = @s.send(method_name)
@@ -165,13 +172,13 @@ module Squeel
165
172
  it 'creates inner joins' do
166
173
  join = @s.inner
167
174
  join.should be_a Join
168
- join.type.should eq Arel::InnerJoin
175
+ join._type.should eq Arel::InnerJoin
169
176
  end
170
177
 
171
178
  it 'creates outer joins' do
172
179
  join = @s.outer
173
180
  join.should be_a Join
174
- join.type.should eq Arel::OuterJoin
181
+ join._type.should eq Arel::OuterJoin
175
182
  end
176
183
 
177
184
  it 'creates functions with #func' do
@@ -179,12 +186,6 @@ module Squeel
179
186
  function.should be_a Function
180
187
  end
181
188
 
182
- it 'creates functions with #[]' do
183
- function = @s[1, 2, 3]
184
- function.should be_a Function
185
- function.args.should eq [1, 2, 3]
186
- end
187
-
188
189
  end
189
190
  end
190
191
  end