squeel_rbg 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.yardopts +3 -0
- data/Gemfile +13 -0
- data/LICENSE +20 -0
- data/README.md +398 -0
- data/Rakefile +19 -0
- data/lib/core_ext/hash.rb +13 -0
- data/lib/core_ext/symbol.rb +39 -0
- data/lib/squeel/adapters/active_record/3.0/association_preload.rb +15 -0
- data/lib/squeel/adapters/active_record/3.0/compat.rb +142 -0
- data/lib/squeel/adapters/active_record/3.0/context.rb +66 -0
- data/lib/squeel/adapters/active_record/3.0/join_association.rb +54 -0
- data/lib/squeel/adapters/active_record/3.0/join_dependency.rb +84 -0
- data/lib/squeel/adapters/active_record/3.0/relation.rb +327 -0
- data/lib/squeel/adapters/active_record/context.rb +66 -0
- data/lib/squeel/adapters/active_record/join_association.rb +44 -0
- data/lib/squeel/adapters/active_record/join_dependency.rb +83 -0
- data/lib/squeel/adapters/active_record/preloader.rb +21 -0
- data/lib/squeel/adapters/active_record/relation.rb +351 -0
- data/lib/squeel/adapters/active_record.rb +28 -0
- data/lib/squeel/configuration.rb +54 -0
- data/lib/squeel/constants.rb +24 -0
- data/lib/squeel/context.rb +67 -0
- data/lib/squeel/dsl.rb +86 -0
- data/lib/squeel/nodes/aliasing.rb +13 -0
- data/lib/squeel/nodes/and.rb +9 -0
- data/lib/squeel/nodes/as.rb +14 -0
- data/lib/squeel/nodes/binary.rb +32 -0
- data/lib/squeel/nodes/function.rb +66 -0
- data/lib/squeel/nodes/join.rb +113 -0
- data/lib/squeel/nodes/key_path.rb +192 -0
- data/lib/squeel/nodes/nary.rb +45 -0
- data/lib/squeel/nodes/not.rb +9 -0
- data/lib/squeel/nodes/operation.rb +32 -0
- data/lib/squeel/nodes/operators.rb +43 -0
- data/lib/squeel/nodes/or.rb +9 -0
- data/lib/squeel/nodes/order.rb +53 -0
- data/lib/squeel/nodes/predicate.rb +71 -0
- data/lib/squeel/nodes/predicate_operators.rb +29 -0
- data/lib/squeel/nodes/stub.rb +125 -0
- data/lib/squeel/nodes/unary.rb +28 -0
- data/lib/squeel/nodes.rb +17 -0
- data/lib/squeel/predicate_methods.rb +14 -0
- data/lib/squeel/version.rb +3 -0
- data/lib/squeel/visitors/attribute_visitor.rb +191 -0
- data/lib/squeel/visitors/base.rb +112 -0
- data/lib/squeel/visitors/predicate_visitor.rb +319 -0
- data/lib/squeel/visitors/symbol_visitor.rb +48 -0
- data/lib/squeel/visitors.rb +3 -0
- data/lib/squeel.rb +28 -0
- data/lib/squeel_rbg.rb +5 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/notes.rb +3 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/console.rb +22 -0
- data/spec/core_ext/symbol_spec.rb +75 -0
- data/spec/helpers/squeel_helper.rb +21 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/squeel/adapters/active_record/context_spec.rb +44 -0
- data/spec/squeel/adapters/active_record/join_association_spec.rb +18 -0
- data/spec/squeel/adapters/active_record/join_dependency_spec.rb +66 -0
- data/spec/squeel/adapters/active_record/relation_spec.rb +627 -0
- data/spec/squeel/dsl_spec.rb +92 -0
- data/spec/squeel/nodes/function_spec.rb +149 -0
- data/spec/squeel/nodes/join_spec.rb +47 -0
- data/spec/squeel/nodes/key_path_spec.rb +100 -0
- data/spec/squeel/nodes/operation_spec.rb +149 -0
- data/spec/squeel/nodes/operators_spec.rb +87 -0
- data/spec/squeel/nodes/order_spec.rb +30 -0
- data/spec/squeel/nodes/predicate_operators_spec.rb +88 -0
- data/spec/squeel/nodes/predicate_spec.rb +50 -0
- data/spec/squeel/nodes/stub_spec.rb +198 -0
- data/spec/squeel/visitors/attribute_visitor_spec.rb +142 -0
- data/spec/squeel/visitors/predicate_visitor_spec.rb +342 -0
- data/spec/squeel/visitors/symbol_visitor_spec.rb +42 -0
- data/spec/support/schema.rb +104 -0
- data/squeel.gemspec +43 -0
- metadata +246 -0
data/lib/squeel/dsl.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Squeel
|
2
|
+
# Interprets DSL blocks, generating various Squeel nodes as appropriate.
|
3
|
+
class DSL
|
4
|
+
|
5
|
+
# We're creating a BlankSlate-type class here, since we want most
|
6
|
+
# method calls to fall through to method_missing.
|
7
|
+
Squeel.evil_things do
|
8
|
+
(instance_methods + private_instance_methods).each do |method|
|
9
|
+
unless method.to_s =~ /^(__|instance_eval)/
|
10
|
+
undef_method method
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Called from an adapter, not directly.
|
16
|
+
# Evaluates a block of Squeel DSL code.
|
17
|
+
#
|
18
|
+
# @example A DSL block that uses instance_eval
|
19
|
+
# Post.where{title == 'Hello world!'}
|
20
|
+
#
|
21
|
+
# @example A DSL block with access to methods from the closure
|
22
|
+
# Post.where{|dsl| dsl.title == local_method(local_var)}
|
23
|
+
#
|
24
|
+
# @yield [dsl] A block of Squeel DSL code, with an optional argument if
|
25
|
+
# access to closure methods is desired.
|
26
|
+
# @return The results of the interpreted DSL code.
|
27
|
+
def self.eval(&block)
|
28
|
+
if block.arity > 0
|
29
|
+
yield self.new(block.binding)
|
30
|
+
else
|
31
|
+
self.new(block.binding).instance_eval(&block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# This isn't normally called directly, but via DSL.eval, which will
|
38
|
+
# pass the block's binding to the new instance, for use with #my.
|
39
|
+
#
|
40
|
+
# @param [Binding] The block's binding.
|
41
|
+
def initialize(caller_binding)
|
42
|
+
@caller = caller_binding.eval 'self'
|
43
|
+
end
|
44
|
+
|
45
|
+
# If you really need to get at an instance variable or method inside
|
46
|
+
# a DSL block, this method will let you do it. It passes a block back
|
47
|
+
# to the DSL's caller for instance_eval.
|
48
|
+
#
|
49
|
+
# It's also pretty evil, so I hope you enjoy using it while I'm burning in
|
50
|
+
# programmer hell.
|
51
|
+
#
|
52
|
+
# @param &block A block to instance_eval against the DSL's caller.
|
53
|
+
# @return
|
54
|
+
def my(&block)
|
55
|
+
@caller.instance_eval &block
|
56
|
+
end
|
57
|
+
|
58
|
+
# Node generation inside DSL blocks.
|
59
|
+
#
|
60
|
+
# @overload node_name
|
61
|
+
# Creates a Stub. Method calls chained from this Stub will determine
|
62
|
+
# what type of node we eventually end up with.
|
63
|
+
# @return [Nodes::Stub] A stub with the name of the method
|
64
|
+
# @overload node_name(klass)
|
65
|
+
# Creates a Join with a polymorphic class matching the given parameter
|
66
|
+
# @param [Class] klass The polymorphic class of the join node
|
67
|
+
# @return [Nodes::Join] A join node with the name of the method and the given class
|
68
|
+
# @overload node_name(first_arg, *other_args)
|
69
|
+
# Creates a Function with the given arguments becoming the function's arguments
|
70
|
+
# @param first_arg The first argument
|
71
|
+
# @param *other_args Optional additional arguments
|
72
|
+
# @return [Nodes::Function] A function node for the given method name with the given arguments
|
73
|
+
def method_missing(method_id, *args)
|
74
|
+
super if method_id == :to_ary
|
75
|
+
|
76
|
+
if args.empty?
|
77
|
+
Nodes::Stub.new method_id
|
78
|
+
elsif (args.size == 1) && (Class === args[0])
|
79
|
+
Nodes::Join.new(method_id, Arel::InnerJoin, args[0])
|
80
|
+
else
|
81
|
+
Nodes::Function.new method_id, args
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'squeel/nodes/binary'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Nodes
|
5
|
+
# A node representing an SQL alias, which will result in same when visited.
|
6
|
+
class As < Binary
|
7
|
+
# @param left The node to be aliased
|
8
|
+
# @param right The alias name
|
9
|
+
def initialize(left, right)
|
10
|
+
@left, @right = left, right
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'squeel/nodes/predicate_operators'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Nodes
|
5
|
+
# A node that represents an operation with two operands.
|
6
|
+
class Binary
|
7
|
+
|
8
|
+
include PredicateOperators
|
9
|
+
|
10
|
+
# The left operand
|
11
|
+
attr_reader :left
|
12
|
+
|
13
|
+
# The right operand
|
14
|
+
attr_reader :right
|
15
|
+
|
16
|
+
# @param left The left operand
|
17
|
+
# @param right The right operand
|
18
|
+
def initialize(left, right)
|
19
|
+
@left, @right = left, right
|
20
|
+
end
|
21
|
+
|
22
|
+
# Comparison with other nodes
|
23
|
+
def eql?(other)
|
24
|
+
self.class == other.class &&
|
25
|
+
self.left == other.left &&
|
26
|
+
self.right == other.right
|
27
|
+
end
|
28
|
+
alias :== :eql?
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'squeel/predicate_methods'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Nodes
|
5
|
+
# A node that represents an SQL function call
|
6
|
+
class Function
|
7
|
+
|
8
|
+
include PredicateMethods
|
9
|
+
include Operators
|
10
|
+
|
11
|
+
alias :== :eq
|
12
|
+
alias :'^' :not_eq
|
13
|
+
alias :'!=' :not_eq if respond_to?(:'!=')
|
14
|
+
alias :>> :in
|
15
|
+
alias :<< :not_in
|
16
|
+
alias :=~ :matches
|
17
|
+
alias :'!~' :does_not_match if respond_to?(:'!~')
|
18
|
+
alias :> :gt
|
19
|
+
alias :>= :gteq
|
20
|
+
alias :< :lt
|
21
|
+
alias :<= :lteq
|
22
|
+
|
23
|
+
# @return [Symbol] The name of the SQL function to be called
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
# @return [Array] The arguments to be passed to the SQL function
|
27
|
+
attr_reader :args
|
28
|
+
|
29
|
+
# @return [String] The SQL function's alias
|
30
|
+
# @return [NilClass] If no alias
|
31
|
+
attr_reader :alias
|
32
|
+
|
33
|
+
# Create a node representing an SQL Function with the given name and arguments
|
34
|
+
# @param [Symbol] name The function name
|
35
|
+
# @param [Array] args The array of arguments to pass to the function.
|
36
|
+
def initialize(name, args)
|
37
|
+
@name, @args = name, args
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set an alias for the function
|
41
|
+
# @param [String, Symbol] The alias name
|
42
|
+
# @return [Function] This function with the new alias value.
|
43
|
+
def as(alias_name)
|
44
|
+
@alias = alias_name.to_s
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def asc
|
49
|
+
Order.new self, 1
|
50
|
+
end
|
51
|
+
|
52
|
+
def desc
|
53
|
+
Order.new self, -1
|
54
|
+
end
|
55
|
+
|
56
|
+
# expand_hash_conditions_for_aggregates assumes our hash keys can be
|
57
|
+
# converted to symbols, so this has to be implemented, but it doesn't
|
58
|
+
# really have to do anything useful.
|
59
|
+
# @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
|
60
|
+
def to_sym
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Squeel
|
2
|
+
module Nodes
|
3
|
+
# A node representing a joined association
|
4
|
+
class Join
|
5
|
+
undef_method :id if method_defined?(:id)
|
6
|
+
|
7
|
+
# @return [Symbol] The join's association name
|
8
|
+
attr_reader :_name
|
9
|
+
|
10
|
+
# @return [Arel::InnerJoin, Arel::OuterJoin] The ARel join type
|
11
|
+
attr_reader :_type
|
12
|
+
|
13
|
+
# @return [Class] The polymorphic belongs_to join class
|
14
|
+
# @return [NilClass] If the join is not a polymorphic belongs_to join
|
15
|
+
attr_reader :_klass
|
16
|
+
|
17
|
+
# Create a new Join node
|
18
|
+
# @param [Symbol] name The association name
|
19
|
+
# @param [Arel::InnerJoin, Arel::OuterJoin] type The ARel join class
|
20
|
+
# @param [Class, String, Symbol] klass The polymorphic belongs_to class or class name
|
21
|
+
def initialize(name, type = Arel::InnerJoin, klass = nil)
|
22
|
+
@_name, @_type = name, type
|
23
|
+
@_klass = convert_to_class(klass) if klass
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the join type to an inner join
|
27
|
+
# @return [Join] The join, with an updated join type.
|
28
|
+
def inner
|
29
|
+
@_type = Arel::InnerJoin
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the join type to an outer join
|
34
|
+
# @return [Join] The join, with an updated join type.
|
35
|
+
def outer
|
36
|
+
@_type = Arel::OuterJoin
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set the polymorphic belongs_to class
|
41
|
+
# @param [Class, String, Symbol] class_or_class_name The polymorphic belongs_to class or class name
|
42
|
+
# @return [Class] The class that's just been set
|
43
|
+
def _klass=(class_or_class_name)
|
44
|
+
@_klass = convert_to_class(class_or_class_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a true value (the class itself) if a polymorphic belongs_to class has been set
|
48
|
+
# @return [NilClass, Class] The class, if present.
|
49
|
+
def polymorphic?
|
50
|
+
@_klass
|
51
|
+
end
|
52
|
+
|
53
|
+
# Compare with other objects
|
54
|
+
def eql?(other)
|
55
|
+
self.class == other.class &&
|
56
|
+
self._name == other._name &&
|
57
|
+
self._type == other._type &&
|
58
|
+
self._klass == other._klass
|
59
|
+
end
|
60
|
+
alias :== :eql?
|
61
|
+
|
62
|
+
# Ensures that a Join can be used as the base of a new KeyPath
|
63
|
+
# @overload node_name
|
64
|
+
# Creates a new KeyPath with this Join as the base and the method_name as the endpoint
|
65
|
+
# @return [KeyPath] The new keypath
|
66
|
+
# @overload node_name(klass)
|
67
|
+
# Creates a new KeyPath with this Join as the base and a polymorphic belongs_to join as the endpoint
|
68
|
+
# @param [Class] klass The polymorphic class for the join
|
69
|
+
# @return [KeyPath] The new keypath
|
70
|
+
def method_missing(method_id, *args)
|
71
|
+
super if method_id == :to_ary
|
72
|
+
if (args.size == 1) && (Class === args[0])
|
73
|
+
KeyPath.new(self, Join.new(method_id, Arel::InnerJoin, args[0]))
|
74
|
+
else
|
75
|
+
KeyPath.new(self, method_id)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return a KeyPath containing only this Join, but flagged as absolute.
|
80
|
+
# This helps Joins behave more like a KeyPath, as anyone using the Squeel
|
81
|
+
# DSL is likely to think of them as such.
|
82
|
+
# @return [KeyPath] An absolute KeyPath, containing only this Join
|
83
|
+
def ~
|
84
|
+
KeyPath.new [], self, true
|
85
|
+
end
|
86
|
+
|
87
|
+
# expand_hash_conditions_for_aggregates assumes our hash keys can be
|
88
|
+
# converted to symbols, so this has to be implemented, but it doesn't
|
89
|
+
# really have to do anything useful.
|
90
|
+
# @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
|
91
|
+
def to_sym
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Convert the given value into a class.
|
98
|
+
# @param [Class, String, Symbol] value The value to be converted
|
99
|
+
# @return [Class] The class after conversion
|
100
|
+
def convert_to_class(value)
|
101
|
+
case value
|
102
|
+
when String, Symbol
|
103
|
+
Kernel.const_get(value)
|
104
|
+
when Class
|
105
|
+
value
|
106
|
+
else
|
107
|
+
raise ArgumentError, "#{value} cannot be converted to a Class"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'squeel/nodes/operators'
|
2
|
+
require 'squeel/nodes/predicate_operators'
|
3
|
+
|
4
|
+
module Squeel
|
5
|
+
module Nodes
|
6
|
+
# A node that stores a path of keys (of Symbol, Stub, or Join values) and
|
7
|
+
# an endpoint. Used similarly to a nested hash.
|
8
|
+
class KeyPath
|
9
|
+
include PredicateOperators
|
10
|
+
include Operators
|
11
|
+
|
12
|
+
# We need some methods to fall through to the endpoint or create a new
|
13
|
+
# stub of the given name
|
14
|
+
%w(id == != =~ !~).each do |method_name|
|
15
|
+
undef_method method_name if method_defined?(method_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Array<Symbol, Stub, Join>] The path
|
19
|
+
attr_reader :path
|
20
|
+
|
21
|
+
# @return The endpoint, either another key as in the path, or a predicate, function, etc.
|
22
|
+
attr_reader :endpoint
|
23
|
+
|
24
|
+
# Create a new KeyPath.
|
25
|
+
# @param [Array, Object] path The intial path. Will be converted to an array if it isn't already.
|
26
|
+
# @param endpoint the endpoint of the KeyPath
|
27
|
+
# @param [Boolean] absolute If the KeyPath should start from the base
|
28
|
+
# or remain relative to whatever location it's found.
|
29
|
+
def initialize(path, endpoint, absolute = false)
|
30
|
+
@path, @endpoint = path, endpoint
|
31
|
+
@path = [@path] unless Array === @path
|
32
|
+
@endpoint = Stub.new(@endpoint) if Symbol === @endpoint
|
33
|
+
@absolute = absolute
|
34
|
+
end
|
35
|
+
|
36
|
+
# Whether or not the KeyPath should be interpreted relative to its current location
|
37
|
+
# (if nested in a Hash, for instance) or as though it's at the base.
|
38
|
+
# @return [Boolean] The flag's value
|
39
|
+
def absolute?
|
40
|
+
@absolute
|
41
|
+
end
|
42
|
+
|
43
|
+
# Object comparison
|
44
|
+
def eql?(other)
|
45
|
+
self.class == other.class &&
|
46
|
+
self.path == other.path &&
|
47
|
+
self.endpoint.eql?(other.endpoint) &&
|
48
|
+
self.absolute? == other.absolute?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
52
|
+
# responds to |
|
53
|
+
# @param other The right hand side of the operation
|
54
|
+
# @return [Or] An Or node with the KeyPath on the left side and the other object on the right.
|
55
|
+
def |(other)
|
56
|
+
endpoint.respond_to?(:|) ? super : no_method_error(:|)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
60
|
+
# responds to &
|
61
|
+
# @param other The right hand side of the operation
|
62
|
+
# @return [And] An And node with the KeyPath on the left side and the other object on the right.
|
63
|
+
def &(other)
|
64
|
+
endpoint.respond_to?(:&) ? super : no_method_error(:&)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
68
|
+
# responds to -@
|
69
|
+
# @return [Not] A not node with the KeyPath as its expression
|
70
|
+
def -@
|
71
|
+
endpoint.respond_to?(:-@) ? super : no_method_error(:-@)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
75
|
+
# responds to +
|
76
|
+
# @param other The right hand side of the operation
|
77
|
+
# @return [Operation] An operation (with operator +) with the KeyPath on its left and the other object on the right.
|
78
|
+
def +(other)
|
79
|
+
endpoint.respond_to?(:+) ? super : no_method_error(:+)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
83
|
+
# responds to -
|
84
|
+
# @param other The right hand side of the operation
|
85
|
+
# @return [Operation] An operation (with operator -) with the KeyPath on its left and the other object on the right.
|
86
|
+
def -(other)
|
87
|
+
endpoint.respond_to?(:-) ? super : no_method_error(:-)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
91
|
+
# responds to *
|
92
|
+
# @param other The right hand side of the operation
|
93
|
+
# @return [Operation] An operation (with operator *) with the KeyPath on its left and the other object on the right.
|
94
|
+
def *(other)
|
95
|
+
endpoint.respond_to?(:*) ? super : no_method_error(:*)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
99
|
+
# responds to /
|
100
|
+
# @param other The right hand side of the operation
|
101
|
+
# @return [Operation] An operation (with operator /) with the KeyPath on its left and the other object on the right.
|
102
|
+
def /(other)
|
103
|
+
endpoint.respond_to?(:/) ? super : no_method_error(:/)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Allow KeyPath to function like its endpoint, in the case where its endpoint
|
107
|
+
# responds to #op
|
108
|
+
# @param [String, Symbol] operator The custom operator
|
109
|
+
# @param other The right hand side of the operation
|
110
|
+
# @return [Operation] An operation with the given custom operator, the KeyPath on its left and the other object on the right.
|
111
|
+
def op(operator, other)
|
112
|
+
endpoint.respond_to?(:op) ? super : no_method_error(:/)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Set the absolute flag on this KeyPath
|
116
|
+
# @return [KeyPath] This keypath, with its absolute flag set to true
|
117
|
+
def ~
|
118
|
+
@absolute = true
|
119
|
+
self
|
120
|
+
end
|
121
|
+
|
122
|
+
# For use with equality tests
|
123
|
+
def hash
|
124
|
+
[self.class, endpoint, *path].hash
|
125
|
+
end
|
126
|
+
|
127
|
+
# expand_hash_conditions_for_aggregates assumes our hash keys can be
|
128
|
+
# converted to symbols, so this has to be implemented, but it doesn't
|
129
|
+
# really have to do anything useful.
|
130
|
+
# @return [NilClass] Just to avoid bombing out on expand_hash_conditions_for_aggregates
|
131
|
+
def to_sym
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
|
135
|
+
# Delegate % to the KeyPath's endpoint, with a bit of special logic for stubs or functions.
|
136
|
+
# @param val The value to be supplied to the created/existing predicate
|
137
|
+
# @return [KeyPath] This KeyPath, with a predicate endpoint containing the given value
|
138
|
+
def %(val)
|
139
|
+
case endpoint
|
140
|
+
when Stub, Function
|
141
|
+
Array === val ? self.in(val) : self.eq(val)
|
142
|
+
self
|
143
|
+
else
|
144
|
+
endpoint % val
|
145
|
+
self
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Array] The KeyPath's path, including its endpoint, as a single array.
|
150
|
+
def path_with_endpoint
|
151
|
+
path + [endpoint]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Impleneted (and aliased to :to_str) to play nicely with ActiveRecord grouped calculations
|
155
|
+
def to_s
|
156
|
+
path.map(&:to_s).join('.') << ".#{endpoint}"
|
157
|
+
end
|
158
|
+
alias :to_str :to_s
|
159
|
+
|
160
|
+
# Appends to the KeyPath or delegates to the endpoint, as appropriate
|
161
|
+
# @return [KeyPath] The updated KeyPath
|
162
|
+
def method_missing(method_id, *args)
|
163
|
+
super if method_id == :to_ary
|
164
|
+
if endpoint.respond_to? method_id
|
165
|
+
@endpoint = @endpoint.send(method_id, *args)
|
166
|
+
self
|
167
|
+
elsif Stub === endpoint || Join === endpoint
|
168
|
+
@path << endpoint
|
169
|
+
if args.empty?
|
170
|
+
@endpoint = Stub.new(method_id)
|
171
|
+
elsif (args.size == 1) && (Class === args[0])
|
172
|
+
@endpoint = Join.new(method_id, Arel::InnerJoin, args[0])
|
173
|
+
else
|
174
|
+
@endpoint = Nodes::Function.new method_id, args
|
175
|
+
end
|
176
|
+
self
|
177
|
+
else
|
178
|
+
super
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# Raises a NoMethodError manually, bypassing #method_missing.
|
185
|
+
# Used by special-case operator overrides.
|
186
|
+
def no_method_error(method_id)
|
187
|
+
raise NoMethodError, "undefined method `#{method_id}' for #{self}:#{self.class}"
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Squeel
|
2
|
+
module Nodes
|
3
|
+
# A node containing multiple children. Just the And node for now.
|
4
|
+
class Nary
|
5
|
+
include PredicateOperators
|
6
|
+
|
7
|
+
# @return [Array] This node's children
|
8
|
+
attr_reader :children
|
9
|
+
|
10
|
+
# Creates a new Nary node with the given array of children
|
11
|
+
# @param [Array] children The node's children
|
12
|
+
def initialize(children)
|
13
|
+
raise ArgumentError, '#{self.class} requires an array' unless Array === children
|
14
|
+
# We don't dup here, as incoming arrays should be created by the
|
15
|
+
# PredicateOperators#& method on other nodes. If you're creating And nodes
|
16
|
+
# manually, by sure that they're new arays.
|
17
|
+
@children = children
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a new child to the node's children
|
21
|
+
# @param other A new child node
|
22
|
+
# @return [Nary] This node, with its updated children.
|
23
|
+
def &(other)
|
24
|
+
@children << other
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Append a new Not node to this node's children
|
29
|
+
# @param other A new child node
|
30
|
+
# @return [Nary] This node, with its new, negated child
|
31
|
+
def -(other)
|
32
|
+
@children << Not.new(other)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Object comparison
|
37
|
+
def eql?(other)
|
38
|
+
self.class == other.class &&
|
39
|
+
self.children == other.children
|
40
|
+
end
|
41
|
+
alias :== :eql?
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'squeel/nodes/function'
|
2
|
+
|
3
|
+
module Squeel
|
4
|
+
module Nodes
|
5
|
+
# An Operation is basically a function with its operands limited, and rearranged,
|
6
|
+
# so Squeel implements it as such.
|
7
|
+
class Operation < Function
|
8
|
+
|
9
|
+
# Create a new Operation with the given operator and left and right operands
|
10
|
+
# @param left The left operand
|
11
|
+
# @param [String, Symbol] operator The operator
|
12
|
+
# @param right The right operand
|
13
|
+
def initialize(left, operator, right)
|
14
|
+
super(operator, [left, right])
|
15
|
+
end
|
16
|
+
|
17
|
+
# An operation should probably call its "function" name an "operator", shouldn't it?
|
18
|
+
alias :operator :name
|
19
|
+
|
20
|
+
# @return The left operand
|
21
|
+
def left
|
22
|
+
args[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return The right operand
|
26
|
+
def right
|
27
|
+
args[1]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Squeel
|
2
|
+
module Nodes
|
3
|
+
# Module containing Operation factory methods for inclusion in Squeel nodes
|
4
|
+
module Operators
|
5
|
+
|
6
|
+
# Factory for a new addition operation with this node as its left operand.
|
7
|
+
# @param value The right operand
|
8
|
+
# @return [Operation] The new addition operation
|
9
|
+
def +(value)
|
10
|
+
Operation.new(self, :+, value)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Factory for a new subtraction operation with this node as its left operand.
|
14
|
+
# @param value The right operand
|
15
|
+
# @return [Operation] The new subtraction operation
|
16
|
+
def -(value)
|
17
|
+
Operation.new(self, :-, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Factory for a new multiplication operation with this node as its left operand.
|
21
|
+
# @param value The right operand
|
22
|
+
# @return [Operation] The new multiplication operation
|
23
|
+
def *(value)
|
24
|
+
Operation.new(self, :*, value)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Factory for a new division operation with this node as its left operand.
|
28
|
+
# @param value The right operand
|
29
|
+
# @return [Operation] The new division operation
|
30
|
+
def /(value)
|
31
|
+
Operation.new(self, :/, value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Factory for a new custom operation with this node as its left operand.
|
35
|
+
# @param value The right operand
|
36
|
+
# @return [Operation] The new operation
|
37
|
+
def op(operator, value)
|
38
|
+
Operation.new(self, operator, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|