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