squeel 0.5.5 → 0.6.0

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 (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