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,35 +3,56 @@ require 'squeel/nodes/predicate_operators'
3
3
 
4
4
  module Squeel
5
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.
6
11
  class Predicate
7
12
 
8
- include PredicateMethods
9
13
  include PredicateOperators
10
14
 
15
+ # @return The right-hand value being considered in this predicate.
11
16
  attr_accessor :value
12
- attr_reader :expr, :method_name
13
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.
14
29
  def initialize(expr, method_name = :eq, value = :__undefined__)
15
30
  @expr, @method_name, @value = expr, method_name, value
16
31
  end
17
32
 
33
+ # Object comparison
18
34
  def eql?(other)
19
35
  self.class.eql?(other.class) &&
20
36
  self.expr.eql?(other.expr) &&
21
37
  self.method_name.eql?(other.method_name) &&
22
38
  self.value.eql?(other.value)
23
39
  end
24
-
25
40
  alias :== :eql?
26
41
 
42
+ # Implemented for equality testing
27
43
  def hash
28
44
  [self.class, expr, method_name, value].hash
29
45
  end
30
46
 
47
+ # Whether the value has been assigned yet.
48
+ # @return [Boolean] Has the value been set?
31
49
  def value?
32
50
  @value != :__undefined__
33
51
  end
34
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
35
56
  def %(val)
36
57
  @value = val
37
58
  self
@@ -40,6 +61,7 @@ module Squeel
40
61
  # expand_hash_conditions_for_aggregates assumes our hash keys can be
41
62
  # converted to symbols, so this has to be implemented, but it doesn't
42
63
  # really have to do anything useful.
64
+ # @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
43
65
  def to_sym
44
66
  nil
45
67
  end
@@ -1,17 +1,29 @@
1
1
  module Squeel
2
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.
3
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
4
10
  def |(other)
5
11
  Or.new(self, other)
6
12
  end
7
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
8
17
  def &(other)
9
18
  And.new([self, other])
10
19
  end
11
20
 
21
+ # Create a new Not node, with this node as its expression
22
+ # @return [Not] The new Not node
12
23
  def -@
13
24
  Not.new(self)
14
25
  end
26
+
15
27
  end
16
28
  end
17
29
  end
@@ -3,111 +3,118 @@ require 'squeel/nodes/operators'
3
3
 
4
4
  module Squeel
5
5
  module Nodes
6
+ # Stub nodes are basically a container for a Symbol that can have handy predicate
7
+ # methods and operators defined on it since doing so on Symbol will incur the
8
+ # nerdrage of many.
6
9
  class Stub
7
10
 
8
11
  include PredicateMethods
9
12
  include Operators
10
13
 
14
+ alias :== :eq
15
+ alias :'^' :not_eq
16
+ alias :'!=' :not_eq if respond_to?(:'!=')
17
+ alias :>> :in
18
+ alias :<< :not_in
19
+ alias :=~ :matches
20
+ alias :'!~' :does_not_match if respond_to?(:'!~')
21
+ alias :> :gt
22
+ alias :>= :gteq
23
+ alias :< :lt
24
+ alias :<= :lteq
25
+
26
+ undef_method :id if method_defined?(:id)
27
+
28
+ # @return [Symbol] The symbol contained by this stub
11
29
  attr_reader :symbol
12
30
 
31
+ # Create a new Stub.
32
+ # @param [Symbol] symbol The symbol that this Stub contains
13
33
  def initialize(symbol)
14
34
  @symbol = symbol
15
35
  end
16
36
 
37
+ # Object comparison
17
38
  def eql?(other)
18
39
  self.class == other.class &&
19
40
  self.symbol == other.symbol
20
41
  end
21
42
 
43
+ # To support object equality tests
22
44
  def hash
23
45
  symbol.hash
24
46
  end
25
47
 
48
+ # @return [Symbol] The symbol this Stub contains.
26
49
  def to_sym
27
50
  symbol
28
51
  end
29
52
 
53
+ # @return [String] The Stub's String equivalent.
30
54
  def to_s
31
55
  symbol.to_s
32
56
  end
33
57
 
58
+ # Create a KeyPath when any undefined method is called on a Stub.
59
+ # @overload node_name
60
+ # Creates a new KeyPath with this Stub as the base and the method_name as the endpoint
61
+ # @return [KeyPath] The new keypath
62
+ # @overload node_name(klass)
63
+ # Creates a new KeyPath with this Stub as the base and a polymorphic belongs_to join as the endpoint
64
+ # @param [Class] klass The polymorphic class for the join
65
+ # @return [KeyPath] The new keypath
34
66
  def method_missing(method_id, *args)
35
67
  super if method_id == :to_ary
36
- if (args.size == 1) && (Class === args[0])
68
+ if args.empty?
69
+ KeyPath.new(self, method_id)
70
+ elsif (args.size == 1) && (Class === args[0])
37
71
  KeyPath.new(self, Join.new(method_id, Arel::InnerJoin, args[0]))
38
72
  else
39
- KeyPath.new(self, method_id)
73
+ KeyPath.new(self, Nodes::Function.new(method_id, args))
40
74
  end
41
75
  end
42
76
 
43
- def ==(value)
44
- Predicate.new self.symbol, :eq, value
45
- end
46
-
47
- # Won't work on Ruby 1.8.x so need to do this conditionally
48
- define_method('!=') do |value|
49
- Predicate.new(self.symbol, :not_eq, value)
50
- end if respond_to?('!=')
51
-
52
- def ^(value)
53
- Predicate.new self.symbol, :not_eq, value
54
- end
55
-
56
- def >>(value)
57
- Predicate.new self.symbol, :in, value
58
- end
59
-
60
- def <<(value)
61
- Predicate.new self.symbol, :not_in, value
62
- end
63
-
64
- def =~(value)
65
- Predicate.new self.symbol, :matches, value
66
- end
67
-
68
- # Won't work on Ruby 1.8.x so need to do this conditionally
69
- define_method('!~') do |value|
70
- Predicate.new(self.symbol, :does_not_match, value)
71
- end if respond_to?('!~')
72
-
73
- def >(value)
74
- Predicate.new self.symbol, :gt, value
75
- end
76
-
77
- def >=(value)
78
- Predicate.new self.symbol, :gteq, value
79
- end
80
-
81
- def <(value)
82
- Predicate.new self.symbol, :lt, value
83
- end
84
-
85
- def <=(value)
86
- Predicate.new self.symbol, :lteq, value
77
+ # Return a KeyPath containing only this Stub, but flagged as absolute.
78
+ # This helps Stubs behave more like a KeyPath, as anyone using the Squeel
79
+ # DSL is likely to think of them as such.
80
+ # @return [KeyPath] An absolute KeyPath, containing only this Stub
81
+ def ~
82
+ KeyPath.new [], self, true
87
83
  end
88
84
 
85
+ # Create an ascending Order node with this Stub's symbol as its expression
86
+ # @return [Order] The new Order node
89
87
  def asc
90
88
  Order.new self.symbol, 1
91
89
  end
92
90
 
91
+ # Create a descending Order node with this Stub's symbol as its expression
92
+ # @return [Order] The new Order node
93
93
  def desc
94
94
  Order.new self.symbol, -1
95
95
  end
96
96
 
97
+ # Create a Function node for a function named the same as this Stub and with the given arguments
98
+ # @return [Function] The new Function node
97
99
  def func(*args)
98
100
  Function.new(self.symbol, args)
99
101
  end
100
102
 
101
- alias :[] :func
102
-
103
+ # Create an inner Join node for the association named by this Stub
104
+ # @return [Join] The new inner Join node
103
105
  def inner
104
106
  Join.new(self.symbol, Arel::InnerJoin)
105
107
  end
106
108
 
109
+ # Create an outer Join node for the association named by this Stub
110
+ # @return [Join] The new outer Join node
107
111
  def outer
108
112
  Join.new(self.symbol, Arel::OuterJoin)
109
113
  end
110
114
 
115
+ # Create a polymorphic Join node for the association named by this Stub,
116
+ # @param [Class] klass The polymorphic belongs_to class for this Join
117
+ # @return [Join] The new polymorphic Join node
111
118
  def of_class(klass)
112
119
  Join.new(self.symbol, Arel::InnerJoin, klass)
113
120
  end
@@ -2,21 +2,27 @@ require 'squeel/nodes/predicate_operators'
2
2
 
3
3
  module Squeel
4
4
  module Nodes
5
+ # A node that contains a single expression.
5
6
  class Unary
7
+
6
8
  include PredicateOperators
7
9
 
10
+ # @return The expression contained in the node
8
11
  attr_reader :expr
9
12
 
13
+ # Create a new Unary node.
14
+ # @param expr The expression to contain inside the node.
10
15
  def initialize(expr)
11
16
  @expr = expr
12
17
  end
13
18
 
19
+ # Object comparison
14
20
  def eql?(other)
15
21
  self.class == other.class &&
16
22
  self.expr == other.expr
17
23
  end
18
-
19
24
  alias :== :eql?
25
+
20
26
  end
21
27
  end
22
28
  end
@@ -1,19 +1,11 @@
1
- require 'squeel/predicate_methods/symbol'
2
- require 'squeel/predicate_methods/stub'
3
- require 'squeel/predicate_methods/predicate'
4
- require 'squeel/predicate_methods/function'
5
-
6
1
  module Squeel
2
+ # Defines Nodes::Predicate factories named for each of the ARel predication methods
7
3
  module PredicateMethods
8
4
 
9
- def self.included(base)
10
- base.send :include, const_get(base.name.split(/::/)[-1].to_sym)
11
- end
12
-
13
5
  Constants::PREDICATES.each do |method_name|
14
6
  class_eval <<-RUBY
15
7
  def #{method_name}(value = :__undefined__)
16
- predicate :#{method_name}, value
8
+ Nodes::Predicate.new self, :#{method_name}, value
17
9
  end
18
10
  RUBY
19
11
  end
@@ -1,3 +1,3 @@
1
1
  module Squeel
2
- VERSION = "0.5.5"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -2,8 +2,18 @@ require 'squeel/visitors/base'
2
2
 
3
3
  module Squeel
4
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.
5
7
  class AttributeVisitor < Base
6
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.
7
17
  def visit_Hash(o, parent)
8
18
  o.map do |k, v|
9
19
  if implies_context_change?(v)
@@ -14,11 +24,19 @@ module Squeel
14
24
  end.flatten
15
25
  end
16
26
 
27
+ # @return [Boolean] Whether the given value implies a context change
28
+ # @param v The value to consider
17
29
  def implies_context_change?(v)
18
- Hash === v || can_accept?(v) ||
19
- (Array === v && !v.empty? && v.all? {|val| can_accept?(val)})
30
+ can_accept?(v)
20
31
  end
21
32
 
33
+ # Change context (by setting the new parent to the result of a #find or
34
+ # #traverse on the key), then accept the given value.
35
+ #
36
+ # @param k The hash key
37
+ # @param v The hash value
38
+ # @param parent The current parent object in the context
39
+ # @return The visited value
22
40
  def visit_with_context_change(k, v, parent)
23
41
  parent = case k
24
42
  when Nodes::KeyPath
@@ -34,32 +52,84 @@ module Squeel
34
52
  end
35
53
  end
36
54
 
55
+ # If there is no context change, we'll just return the value unchanged,
56
+ # currently. Is this really the right behavior? I don't think so, but
57
+ # it works in this case.
58
+ #
59
+ # @param k The hash key
60
+ # @param v The hash value
61
+ # @param parent The current parent object in the context
62
+ # @return The same value we just received. Yeah, this method's pretty pointless,
63
+ # for now, and only here for consistency's sake.
37
64
  def visit_without_context_change(k, v, parent)
38
65
  v
39
66
  end
40
67
 
68
+ # Visit elements of an array that it's possible to visit -- leave other
69
+ # elements untouched.
70
+ #
71
+ # @param [Array] o The array to visit
72
+ # @param parent The array's parent within the context
73
+ # @return [Array] The flattened array with elements visited
41
74
  def visit_Array(o, parent)
42
75
  o.map { |v| can_accept?(v) ? accept(v, parent) : v }.flatten
43
76
  end
44
77
 
78
+ # Visit a symbol. This will return an attribute named after the symbol against
79
+ # the current parent's contextualized table.
80
+ #
81
+ # @param [Symbol] o The symbol to visit
82
+ # @param parent The symbol's parent within the context
83
+ # @return [Arel::Attribute] An attribute on the contextualized parent table
45
84
  def visit_Symbol(o, parent)
46
85
  contextualize(parent)[o]
47
86
  end
48
87
 
88
+ # Visit a stub. This will return an attribute named after the stub against
89
+ # the current parent's contextualized table.
90
+ #
91
+ # @param [Nodes::Stub] o The stub to visit
92
+ # @param parent The stub's parent within the context
93
+ # @return [Arel::Attribute] An attribute on the contextualized parent table
49
94
  def visit_Squeel_Nodes_Stub(o, parent)
50
95
  contextualize(parent)[o.symbol]
51
96
  end
52
97
 
98
+ # Visit a keypath. This will traverse the keypath's "path", setting a new
99
+ # parent as though the keypath's endpoint was in a deeply-nested hash,
100
+ # then visit the endpoint with the new parent.
101
+ #
102
+ # @param [Nodes::KeyPath] o The keypath to visit
103
+ # @param parent The keypath's parent within the context
104
+ # @return The visited endpoint, with the parent from the KeyPath's path.
53
105
  def visit_Squeel_Nodes_KeyPath(o, parent)
54
106
  parent = traverse(o, parent)
55
107
 
56
108
  accept(o.endpoint, parent)
57
109
  end
58
110
 
111
+ # Visit an Order node.
112
+ #
113
+ # @param [Nodes::Order] o The order node to visit
114
+ # @param parent The node's parent within the context
115
+ # @return [Arel::Nodes::Ordering] An ascending or desending ordering
59
116
  def visit_Squeel_Nodes_Order(o, parent)
60
117
  accept(o.expr, parent).send(o.descending? ? :desc : :asc)
61
118
  end
62
119
 
120
+ # Visit a Function node. Each function argument will be accepted or
121
+ # contextualized if appropriate. Keep in mind that this occurs with
122
+ # the current parent within the context.
123
+ #
124
+ # @example A function as the endpoint of a keypath
125
+ # Person.joins{children}.order{children.coalesce(name, '<no name>')}
126
+ # # => SELECT "people".* FROM "people"
127
+ # INNER JOIN "people" "children_people"
128
+ # ON "children_people"."parent_id" = "people"."id"
129
+ # ORDER BY coalesce("children_people"."name", '<no name>')
130
+ #
131
+ # @param [Nodes::Function] o The function node to visit
132
+ # @param parent The node's parent within the context
63
133
  def visit_Squeel_Nodes_Function(o, parent)
64
134
  args = o.args.map do |arg|
65
135
  case arg
@@ -68,12 +138,18 @@ module Squeel
68
138
  when Symbol, Nodes::Stub
69
139
  Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
70
140
  else
71
- quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
141
+ quote arg
72
142
  end
73
143
  end
74
144
  Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
75
145
  end
76
146
 
147
+ # Visit an Operation node. Each operand will be accepted or
148
+ # contextualized if appropriate. Keep in mind that this occurs with
149
+ # the current parent within the context.
150
+ #
151
+ # @param [Nodes::Operation] o The operation node to visit
152
+ # @param parent The node's parent within the context
77
153
  def visit_Squeel_Nodes_Operation(o, parent)
78
154
  args = o.args.map do |arg|
79
155
  case arg
@@ -82,7 +158,7 @@ module Squeel
82
158
  when Symbol, Nodes::Stub
83
159
  Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
84
160
  else
85
- quoted?(arg) ? Arel.sql(arel_visitor.accept arg) : arg
161
+ quote arg
86
162
  end
87
163
  end
88
164