squeel_rbg 0.8.2

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 (80) hide show
  1. data/.gitignore +4 -0
  2. data/.yardopts +3 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +20 -0
  5. data/README.md +398 -0
  6. data/Rakefile +19 -0
  7. data/lib/core_ext/hash.rb +13 -0
  8. data/lib/core_ext/symbol.rb +39 -0
  9. data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
  10. data/lib/squeel/adapters/active_record/3.0/compat.rb +142 -0
  11. data/lib/squeel/adapters/active_record/3.0/context.rb +66 -0
  12. data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
  13. data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
  14. data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
  15. data/lib/squeel/adapters/active_record/context.rb +66 -0
  16. data/lib/squeel/adapters/active_record/join_association.rb +44 -0
  17. data/lib/squeel/adapters/active_record/join_dependency.rb +83 -0
  18. data/lib/squeel/adapters/active_record/preloader.rb +21 -0
  19. data/lib/squeel/adapters/active_record/relation.rb +351 -0
  20. data/lib/squeel/adapters/active_record.rb +28 -0
  21. data/lib/squeel/configuration.rb +54 -0
  22. data/lib/squeel/constants.rb +24 -0
  23. data/lib/squeel/context.rb +67 -0
  24. data/lib/squeel/dsl.rb +86 -0
  25. data/lib/squeel/nodes/aliasing.rb +13 -0
  26. data/lib/squeel/nodes/and.rb +9 -0
  27. data/lib/squeel/nodes/as.rb +14 -0
  28. data/lib/squeel/nodes/binary.rb +32 -0
  29. data/lib/squeel/nodes/function.rb +66 -0
  30. data/lib/squeel/nodes/join.rb +113 -0
  31. data/lib/squeel/nodes/key_path.rb +192 -0
  32. data/lib/squeel/nodes/nary.rb +45 -0
  33. data/lib/squeel/nodes/not.rb +9 -0
  34. data/lib/squeel/nodes/operation.rb +32 -0
  35. data/lib/squeel/nodes/operators.rb +43 -0
  36. data/lib/squeel/nodes/or.rb +9 -0
  37. data/lib/squeel/nodes/order.rb +53 -0
  38. data/lib/squeel/nodes/predicate.rb +71 -0
  39. data/lib/squeel/nodes/predicate_operators.rb +29 -0
  40. data/lib/squeel/nodes/stub.rb +125 -0
  41. data/lib/squeel/nodes/unary.rb +28 -0
  42. data/lib/squeel/nodes.rb +17 -0
  43. data/lib/squeel/predicate_methods.rb +14 -0
  44. data/lib/squeel/version.rb +3 -0
  45. data/lib/squeel/visitors/attribute_visitor.rb +191 -0
  46. data/lib/squeel/visitors/base.rb +112 -0
  47. data/lib/squeel/visitors/predicate_visitor.rb +319 -0
  48. data/lib/squeel/visitors/symbol_visitor.rb +48 -0
  49. data/lib/squeel/visitors.rb +3 -0
  50. data/lib/squeel.rb +28 -0
  51. data/lib/squeel_rbg.rb +5 -0
  52. data/spec/blueprints/articles.rb +5 -0
  53. data/spec/blueprints/comments.rb +5 -0
  54. data/spec/blueprints/notes.rb +3 -0
  55. data/spec/blueprints/people.rb +4 -0
  56. data/spec/blueprints/tags.rb +3 -0
  57. data/spec/console.rb +22 -0
  58. data/spec/core_ext/symbol_spec.rb +75 -0
  59. data/spec/helpers/squeel_helper.rb +21 -0
  60. data/spec/spec_helper.rb +66 -0
  61. data/spec/squeel/adapters/active_record/context_spec.rb +44 -0
  62. data/spec/squeel/adapters/active_record/join_association_spec.rb +18 -0
  63. data/spec/squeel/adapters/active_record/join_dependency_spec.rb +66 -0
  64. data/spec/squeel/adapters/active_record/relation_spec.rb +627 -0
  65. data/spec/squeel/dsl_spec.rb +92 -0
  66. data/spec/squeel/nodes/function_spec.rb +149 -0
  67. data/spec/squeel/nodes/join_spec.rb +47 -0
  68. data/spec/squeel/nodes/key_path_spec.rb +100 -0
  69. data/spec/squeel/nodes/operation_spec.rb +149 -0
  70. data/spec/squeel/nodes/operators_spec.rb +87 -0
  71. data/spec/squeel/nodes/order_spec.rb +30 -0
  72. data/spec/squeel/nodes/predicate_operators_spec.rb +88 -0
  73. data/spec/squeel/nodes/predicate_spec.rb +50 -0
  74. data/spec/squeel/nodes/stub_spec.rb +198 -0
  75. data/spec/squeel/visitors/attribute_visitor_spec.rb +142 -0
  76. data/spec/squeel/visitors/predicate_visitor_spec.rb +342 -0
  77. data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
  78. data/spec/support/schema.rb +104 -0
  79. data/squeel.gemspec +43 -0
  80. metadata +246 -0
@@ -0,0 +1,53 @@
1
+ module Squeel
2
+ module Nodes
3
+ # A node that represents SQL orderings, such as "people.id DESC"
4
+ class Order
5
+ # @return The expression being ordered on. Might be an attribute, function, or operation
6
+ attr_reader :expr
7
+
8
+ # @return [Fixnum] 1 or -1, depending on ascending or descending direction, respectively
9
+ attr_reader :direction
10
+
11
+ # Create a new Order node with the given expression and direction
12
+ # @param expr The expression to order on
13
+ # @param [Fixnum] direction 1 or -1, depending on the desired sort direction
14
+ def initialize(expr, direction = 1)
15
+ raise ArgumentError, "Direction #{direction} is not valid. Must be -1 or 1." unless [-1,1].include? direction
16
+ @expr, @direction = expr, direction
17
+ end
18
+
19
+ # Set this node's direction to ascending
20
+ # @return [Order] This order node with an ascending direction
21
+ def asc
22
+ @direction = 1
23
+ self
24
+ end
25
+
26
+ # Set this node's direction to descending
27
+ # @return [Order] This order node with a descending direction
28
+ def desc
29
+ @direction = -1
30
+ self
31
+ end
32
+
33
+ # Whether or not this node represents an ascending order
34
+ # @return [Boolean] True if the order is ascending
35
+ def ascending?
36
+ @direction == 1
37
+ end
38
+
39
+ # Whether or not this node represents a descending order
40
+ # @return [Boolean] True if the order is descending
41
+ def descending?
42
+ @direction == -1
43
+ end
44
+
45
+ # Reverse the node's direction
46
+ # @return [Order] This node with a reversed direction
47
+ def reverse!
48
+ @direction = - @direction
49
+ self
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,71 @@
1
+ require 'squeel/predicate_methods'
2
+ require 'squeel/nodes/predicate_operators'
3
+
4
+ module Squeel
5
+ module Nodes
6
+ # This node is essentially a container that will result in ARel predicate nodes
7
+ # once visited. It stores the expression (normally an attribute name, function, or
8
+ # operation), the ARel predicate method name, and a value. these are then interpreted
9
+ # when visited by the PredicateVisitor to generate a condition against the appropriate
10
+ # columns.
11
+ class Predicate
12
+
13
+ include PredicateOperators
14
+
15
+ # @return The right-hand value being considered in this predicate.
16
+ attr_accessor :value
17
+
18
+ # @return The expression on the left side of this predicate.
19
+ attr_reader :expr
20
+
21
+ # @return [Symbol] The ARel "predication" method name, such as eq, matches, etc.
22
+ attr_reader :method_name
23
+
24
+ # Create a new Predicate node with the given expression, method name, and value
25
+ # @param expr The expression for the left hand side of the predicate.
26
+ # @param [Symbol] method_name The ARel predication method
27
+ # @param value An optional value. If not given, one will need to be supplied
28
+ # before the node can be visited properly.
29
+ def initialize(expr, method_name = :eq, value = :__undefined__)
30
+ @expr, @method_name, @value = expr, method_name, value
31
+ end
32
+
33
+ # Object comparison
34
+ def eql?(other)
35
+ self.class.eql?(other.class) &&
36
+ self.expr.eql?(other.expr) &&
37
+ self.method_name.eql?(other.method_name) &&
38
+ self.value.eql?(other.value)
39
+ end
40
+ alias :== :eql?
41
+
42
+ # Implemented for equality testing
43
+ def hash
44
+ [self.class, expr, method_name, value].hash
45
+ end
46
+
47
+ # Whether the value has been assigned yet.
48
+ # @return [Boolean] Has the value been set?
49
+ def value?
50
+ @value != :__undefined__
51
+ end
52
+
53
+ # Set the value for this predicate
54
+ # @param val The value to be set
55
+ # @return [Predicate] This predicate, with its new value set
56
+ def %(val)
57
+ @value = val
58
+ self
59
+ end
60
+
61
+ # expand_hash_conditions_for_aggregates assumes our hash keys can be
62
+ # converted to symbols, so this has to be implemented, but it doesn't
63
+ # really have to do anything useful.
64
+ # @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
65
+ def to_sym
66
+ nil
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ module Squeel
2
+ module Nodes
3
+ # Operators that act as factories for Or, And, and Not nodes for inclusion
4
+ # in classes which can be contained inside these nodes.
5
+ module PredicateOperators
6
+
7
+ # Create a new Or node, with this node as its left-hand node.
8
+ # @param other The right-hand node for the Or
9
+ # @return [Or] The new Or node
10
+ def |(other)
11
+ Or.new(self, other)
12
+ end
13
+
14
+ # Create a new And node, with this node as its left-hand node.
15
+ # @param other The right-hand node for the And
16
+ # @return [And] The new And node
17
+ def &(other)
18
+ And.new([self, other])
19
+ end
20
+
21
+ # Create a new Not node, with this node as its expression
22
+ # @return [Not] The new Not node
23
+ def -@
24
+ Not.new(self)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,125 @@
1
+ require 'squeel/predicate_methods'
2
+ require 'squeel/nodes/operators'
3
+ require 'squeel/nodes/aliasing'
4
+
5
+ module Squeel
6
+ module Nodes
7
+ # Stub nodes are basically a container for a Symbol that can have handy predicate
8
+ # methods and operators defined on it since doing so on Symbol will incur the
9
+ # nerdrage of many.
10
+ class Stub
11
+ include PredicateMethods
12
+ include Operators
13
+ include Aliasing
14
+
15
+ alias :== :eq
16
+ alias :'^' :not_eq
17
+ alias :'!=' :not_eq if respond_to?(:'!=')
18
+ alias :>> :in
19
+ alias :<< :not_in
20
+ alias :=~ :matches
21
+ alias :'!~' :does_not_match if respond_to?(:'!~')
22
+ alias :> :gt
23
+ alias :>= :gteq
24
+ alias :< :lt
25
+ alias :<= :lteq
26
+
27
+ undef_method :id if method_defined?(:id)
28
+
29
+ # @return [Symbol] The symbol contained by this stub
30
+ attr_reader :symbol
31
+
32
+ # Create a new Stub.
33
+ # @param [Symbol] symbol The symbol that this Stub contains
34
+ def initialize(symbol)
35
+ @symbol = symbol
36
+ end
37
+
38
+ # Object comparison
39
+ def eql?(other)
40
+ self.class == other.class &&
41
+ self.symbol == other.symbol
42
+ end
43
+
44
+ # To support object equality tests
45
+ def hash
46
+ symbol.hash
47
+ end
48
+
49
+ # @return [Symbol] The symbol this Stub contains.
50
+ def to_sym
51
+ symbol
52
+ end
53
+
54
+ # @return [String] The Stub's String equivalent.
55
+ def to_s
56
+ symbol.to_s
57
+ end
58
+
59
+ # Create a KeyPath when any undefined method is called on a Stub.
60
+ # @overload node_name
61
+ # Creates a new KeyPath with this Stub as the base and the method_name as the endpoint
62
+ # @return [KeyPath] The new keypath
63
+ # @overload node_name(klass)
64
+ # Creates a new KeyPath with this Stub as the base and a polymorphic belongs_to join as the endpoint
65
+ # @param [Class] klass The polymorphic class for the join
66
+ # @return [KeyPath] The new keypath
67
+ def method_missing(method_id, *args)
68
+ super if method_id == :to_ary
69
+ if args.empty?
70
+ KeyPath.new(self, method_id)
71
+ elsif (args.size == 1) && (Class === args[0])
72
+ KeyPath.new(self, Join.new(method_id, Arel::InnerJoin, args[0]))
73
+ else
74
+ KeyPath.new(self, Nodes::Function.new(method_id, args))
75
+ end
76
+ end
77
+
78
+ # Return a KeyPath containing only this Stub, but flagged as absolute.
79
+ # This helps Stubs behave more like a KeyPath, as anyone using the Squeel
80
+ # DSL is likely to think of them as such.
81
+ # @return [KeyPath] An absolute KeyPath, containing only this Stub
82
+ def ~
83
+ KeyPath.new [], self, true
84
+ end
85
+
86
+ # Create an ascending Order node with this Stub's symbol as its expression
87
+ # @return [Order] The new Order node
88
+ def asc
89
+ Order.new self.symbol, 1
90
+ end
91
+
92
+ # Create a descending Order node with this Stub's symbol as its expression
93
+ # @return [Order] The new Order node
94
+ def desc
95
+ Order.new self.symbol, -1
96
+ end
97
+
98
+ # Create a Function node for a function named the same as this Stub and with the given arguments
99
+ # @return [Function] The new Function node
100
+ def func(*args)
101
+ Function.new(self.symbol, args)
102
+ end
103
+
104
+ # Create an inner Join node for the association named by this Stub
105
+ # @return [Join] The new inner Join node
106
+ def inner
107
+ Join.new(self.symbol, Arel::InnerJoin)
108
+ end
109
+
110
+ # Create an outer Join node for the association named by this Stub
111
+ # @return [Join] The new outer Join node
112
+ def outer
113
+ Join.new(self.symbol, Arel::OuterJoin)
114
+ end
115
+
116
+ # Create a polymorphic Join node for the association named by this Stub,
117
+ # @param [Class] klass The polymorphic belongs_to class for this Join
118
+ # @return [Join] The new polymorphic Join node
119
+ def of_class(klass)
120
+ Join.new(self.symbol, Arel::InnerJoin, klass)
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,28 @@
1
+ require 'squeel/nodes/predicate_operators'
2
+
3
+ module Squeel
4
+ module Nodes
5
+ # A node that contains a single expression.
6
+ class Unary
7
+
8
+ include PredicateOperators
9
+
10
+ # @return The expression contained in the node
11
+ attr_reader :expr
12
+
13
+ # Create a new Unary node.
14
+ # @param expr The expression to contain inside the node.
15
+ def initialize(expr)
16
+ @expr = expr
17
+ end
18
+
19
+ # Object comparison
20
+ def eql?(other)
21
+ self.class == other.class &&
22
+ self.expr == other.expr
23
+ end
24
+ alias :== :eql?
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ module Squeel
2
+ # Namespace for the nodes created by Squeel::DSL, and
3
+ # evaluated by Squeel::Visitors classes
4
+ module Nodes
5
+ end
6
+ end
7
+ require 'squeel/nodes/stub'
8
+ require 'squeel/nodes/key_path'
9
+ require 'squeel/nodes/predicate'
10
+ require 'squeel/nodes/function'
11
+ require 'squeel/nodes/operation'
12
+ require 'squeel/nodes/order'
13
+ require 'squeel/nodes/and'
14
+ require 'squeel/nodes/or'
15
+ require 'squeel/nodes/as'
16
+ require 'squeel/nodes/not'
17
+ require 'squeel/nodes/join'
@@ -0,0 +1,14 @@
1
+ module Squeel
2
+ # Defines Nodes::Predicate factories named for each of the ARel predication methods
3
+ module PredicateMethods
4
+
5
+ Constants::PREDICATES.each do |method_name|
6
+ class_eval <<-RUBY
7
+ def #{method_name}(value = :__undefined__)
8
+ Nodes::Predicate.new self, :#{method_name}, value
9
+ end
10
+ RUBY
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Squeel
2
+ VERSION = "0.8.2"
3
+ end
@@ -0,0 +1,191 @@
1
+ require 'squeel/visitors/base'
2
+
3
+ module Squeel
4
+ module Visitors
5
+ # A visitor that tries to convert visited nodes into Arel::Attributes
6
+ # or other nodes that can be used for grouping, ordering, and the like.
7
+ class AttributeVisitor < Base
8
+
9
+ private
10
+
11
+ # Visit a Hash. This entails iterating through each key and value and
12
+ # visiting each value in turn.
13
+ #
14
+ # @param [Hash] o The Hash to visit
15
+ # @param parent The current parent object in the context
16
+ # @return [Array] An array of values for use in an ordering, grouping, etc.
17
+ def visit_Hash(o, parent)
18
+ o.map do |k, v|
19
+ if implies_context_change?(v)
20
+ visit_with_context_change(k, v, parent)
21
+ else
22
+ visit_without_context_change(k, v, parent)
23
+ end
24
+ end.flatten
25
+ end
26
+
27
+ # Visit elements of an array that it's possible to visit -- leave other
28
+ # elements untouched.
29
+ #
30
+ # @param [Array] o The array to visit
31
+ # @param parent The array's parent within the context
32
+ # @return [Array] The flattened array with elements visited
33
+ def visit_Array(o, parent)
34
+ o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
35
+ end
36
+
37
+ # Visit a symbol. This will return an attribute named after the symbol against
38
+ # the current parent's contextualized table.
39
+ #
40
+ # @param [Symbol] o The symbol to visit
41
+ # @param parent The symbol's parent within the context
42
+ # @return [Arel::Attribute] An attribute on the contextualized parent table
43
+ def visit_Symbol(o, parent)
44
+ contextualize(parent)[o]
45
+ end
46
+
47
+ # Visit a stub. This will return an attribute named after the stub against
48
+ # the current parent's contextualized table.
49
+ #
50
+ # @param [Nodes::Stub] o The stub to visit
51
+ # @param parent The stub's parent within the context
52
+ # @return [Arel::Attribute] An attribute on the contextualized parent table
53
+ def visit_Squeel_Nodes_Stub(o, parent)
54
+ contextualize(parent)[o.symbol]
55
+ end
56
+
57
+ # Visit a keypath. This will traverse the keypath's "path", setting a new
58
+ # parent as though the keypath's endpoint was in a deeply-nested hash,
59
+ # then visit the endpoint with the new parent.
60
+ #
61
+ # @param [Nodes::KeyPath] o The keypath to visit
62
+ # @param parent The keypath's parent within the context
63
+ # @return The visited endpoint, with the parent from the KeyPath's path.
64
+ def visit_Squeel_Nodes_KeyPath(o, parent)
65
+ parent = traverse(o, parent)
66
+
67
+ accept(o.endpoint, parent)
68
+ end
69
+
70
+ # Visit an Order node.
71
+ #
72
+ # @param [Nodes::Order] o The order node to visit
73
+ # @param parent The node's parent within the context
74
+ # @return [Arel::Nodes::Ordering] An ascending or desending ordering
75
+ def visit_Squeel_Nodes_Order(o, parent)
76
+ accept(o.expr, parent).send(o.descending? ? :desc : :asc)
77
+ end
78
+
79
+ # Visit a Function node. Each function argument will be accepted or
80
+ # contextualized if appropriate. Keep in mind that this occurs with
81
+ # the current parent within the context.
82
+ #
83
+ # @example A function as the endpoint of a keypath
84
+ # Person.joins{children}.order{children.coalesce(name, '<no name>')}
85
+ # # => SELECT "people".* FROM "people"
86
+ # INNER JOIN "people" "children_people"
87
+ # ON "children_people"."parent_id" = "people"."id"
88
+ # ORDER BY coalesce("children_people"."name", '<no name>')
89
+ #
90
+ # @param [Nodes::Function] o The function node to visit
91
+ # @param parent The node's parent within the context
92
+ def visit_Squeel_Nodes_Function(o, parent)
93
+ args = o.args.map do |arg|
94
+ case arg
95
+ when Nodes::Function, Nodes::KeyPath
96
+ accept(arg, parent)
97
+ when Symbol, Nodes::Stub
98
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
99
+ else
100
+ quote arg
101
+ end
102
+ end
103
+ Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
104
+ end
105
+
106
+ # Visit an Operation node. Each operand will be accepted or
107
+ # contextualized if appropriate. Keep in mind that this occurs with
108
+ # the current parent within the context.
109
+ #
110
+ # @param [Nodes::Operation] o The operation node to visit
111
+ # @param parent The node's parent within the context
112
+ def visit_Squeel_Nodes_Operation(o, parent)
113
+ args = o.args.map do |arg|
114
+ case arg
115
+ when Nodes::Function
116
+ accept(arg, parent)
117
+ when Symbol, Nodes::Stub
118
+ Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
119
+ else
120
+ quote arg
121
+ end
122
+ end
123
+
124
+ op = case o.operator
125
+ when :+
126
+ Arel::Nodes::Addition.new(args[0], args[1])
127
+ when :-
128
+ Arel::Nodes::Subtraction.new(args[0], args[1])
129
+ when :*
130
+ Arel::Nodes::Multiplication.new(args[0], args[1])
131
+ when :/
132
+ Arel::Nodes::Division.new(args[0], args[1])
133
+ else
134
+ Arel.sql("#{arel_visitor.accept(args[0])} #{o.operator} #{arel_visitor.accept(args[1])}")
135
+ end
136
+ o.alias ? op.as(o.alias) : op
137
+ end
138
+
139
+ # Visit a Squeel As node, resulting in am ARel As node.
140
+ #
141
+ # @param [Nodes::As] The As node to visit
142
+ # @param parent The parent object in the context
143
+ # @return [Arel::Nodes::As] The resulting as node.
144
+ def visit_Squeel_Nodes_As(o, parent)
145
+ accept(o.left, parent).as(o.right)
146
+ end
147
+
148
+ # @return [Boolean] Whether the given value implies a context change
149
+ # @param v The value to consider
150
+ def implies_context_change?(v)
151
+ can_accept?(v)
152
+ end
153
+
154
+ # Change context (by setting the new parent to the result of a #find or
155
+ # #traverse on the key), then accept the given value.
156
+ #
157
+ # @param k The hash key
158
+ # @param v The hash value
159
+ # @param parent The current parent object in the context
160
+ # @return The visited value
161
+ def visit_with_context_change(k, v, parent)
162
+ parent = case k
163
+ when Nodes::KeyPath
164
+ traverse(k, parent, true)
165
+ else
166
+ find(k, parent)
167
+ end
168
+
169
+ if Array === v
170
+ v.map {|val| accept(val, parent || k)}
171
+ else
172
+ can_accept?(v) ? accept(v, parent || k) : v
173
+ end
174
+ end
175
+
176
+ # If there is no context change, we'll just return the value unchanged,
177
+ # currently. Is this really the right behavior? I don't think so, but
178
+ # it works in this case.
179
+ #
180
+ # @param k The hash key
181
+ # @param v The hash value
182
+ # @param parent The current parent object in the context
183
+ # @return The same value we just received. Yeah, this method's pretty pointless,
184
+ # for now, and only here for consistency's sake.
185
+ def visit_without_context_change(k, v, parent)
186
+ v
187
+ end
188
+
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,112 @@
1
+ require 'active_support/core_ext/module'
2
+ require 'squeel/nodes'
3
+
4
+ module Squeel
5
+ module Visitors
6
+ # The Base visitor class, containing the default behavior common to subclasses.
7
+ class Base
8
+ attr_accessor :context
9
+ delegate :contextualize, :find, :traverse, :engine, :arel_visitor, :to => :context
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.
15
+ def initialize(context = nil)
16
+ @context = context
17
+ end
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.
25
+ def accept(object, parent = context.base)
26
+ visit(object, parent)
27
+ end
28
+
29
+ # @param object The object to check
30
+ # @return [Boolean] Whether or not the visitor can accept the given object
31
+ def can_accept?(object)
32
+ self.class.can_accept? object
33
+ end
34
+
35
+ # @param object The object to check
36
+ # @return [Boolean] Whether or not visitors of this class can accept the given object
37
+ def self.can_accept?(object)
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]
44
+ end
45
+
46
+ private
47
+
48
+ # A hash that caches the method name to use for a visitor for a given class
49
+ DISPATCH = Hash.new do |hash, klass|
50
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
51
+ end
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.
61
+ def quoted?(object)
62
+ case object
63
+ when Arel::Nodes::SqlLiteral, Bignum, Fixnum, Arel::SelectManager
64
+ false
65
+ else
66
+ true
67
+ end
68
+ end
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
97
+ def visit(object, parent)
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
108
+ end
109
+
110
+ end
111
+ end
112
+ end