panda_query 0.3.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.
- data/LICENSE +19 -0
- data/README +153 -0
- data/Rakefile +13 -0
- data/lib/panda/build.rb +51 -0
- data/lib/panda/core_ext/hash.rb +7 -0
- data/lib/panda/expressers/base.rb +30 -0
- data/lib/panda/expressers/examples/active_record_expresser.rb +20 -0
- data/lib/panda/expressers/examples/sql_expresser.rb +33 -0
- data/lib/panda/expressers/inspector.rb +27 -0
- data/lib/panda/expressers/tree_expresser.rb +46 -0
- data/lib/panda/expressions.rb +205 -0
- data/lib/panda/query.rb +96 -0
- data/lib/panda_query.rb +3 -0
- data/test/panda/build_case.rb +30 -0
- data/test/panda/expressions/boolean_case.rb +42 -0
- data/test/panda/expressions/comparison_case.rb +49 -0
- data/test/panda/expressions/expression_case.rb +63 -0
- data/test/panda/expressions/subject_case.rb +35 -0
- data/test/panda/expressions_suite.rb +7 -0
- data/test/panda/query_case.rb +155 -0
- data/test/panda_suite.rb +6 -0
- metadata +74 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008 Christian Herschel Stevenson, Persapient Systems
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
= PANDA Query
|
2
|
+
|
3
|
+
The PANDA Query library provides a generic query object, which is basically an
|
4
|
+
immutable hash whose _where_ clause is represented as a boolean/comparison
|
5
|
+
expression AST, or *PANDA* (<b>P</b>ainlessly <b>A</b>llocated _via_ "<b>N</b>ative
|
6
|
+
<b>D</b>SL" <b>A</b>ST).
|
7
|
+
|
8
|
+
The Goal of PANDA is to make it possible to build _where_ style expression ASTs
|
9
|
+
'painlessly' on the fly (in a single line of code) that are highly readable, with
|
10
|
+
code that closely resembles the expression being built. This is accomplished
|
11
|
+
with a kind of internal (or '_native_') DSL implemented in pure ruby via method
|
12
|
+
calls on Panda objects.
|
13
|
+
|
14
|
+
PANDA is basically a variation of the Parser-less Interpreter/Internal DSL patterns
|
15
|
+
as described in Russ Olsen's {Design Patterns in Ruby}[http://designpatternsinruby.com].
|
16
|
+
|
17
|
+
== The Panda AST
|
18
|
+
|
19
|
+
In order to fully grasp the painlessness of building a PANDA, it would first be
|
20
|
+
wise to see the _painful_ example first.
|
21
|
+
|
22
|
+
In the following example I build a PANDA node by node the hard way in order to
|
23
|
+
provide a little insight on the relatively simple anatomy of a Panda tree. The
|
24
|
+
statement I build here would be the SQL <em>where clause</em> equivalent to
|
25
|
+
"<tt>first_name = 'Milton' AND last_name = 'Waddams' OR handle LIKE '%milwad%'
|
26
|
+
</tt>".
|
27
|
+
|
28
|
+
require 'panda/expressions'
|
29
|
+
|
30
|
+
s1 = Panda::Subject.new :first_name
|
31
|
+
c1 = Panda::Comparison.new s1, :==, 'Milton'
|
32
|
+
s2 = Panda::Subject.new :last_name
|
33
|
+
c2 = Panda::Comparison.new s2, :==, 'Waddams'
|
34
|
+
b1 = Panda::Boolean.new c1, :'&', c2
|
35
|
+
|
36
|
+
s3 = Panda::Subject.new :handle
|
37
|
+
c3 = Panda::Comparison.new s3, :like, '%milwad%'
|
38
|
+
|
39
|
+
root = Panda::Boolean.new b1, :'|', c3
|
40
|
+
|
41
|
+
To get a better look at what was just built, we can execute the following code:
|
42
|
+
|
43
|
+
require 'panda/expressers/tree_expresser'
|
44
|
+
|
45
|
+
puts Panda::Expressers::TreeExpresser.express(root) # puts the following..
|
46
|
+
:|
|
47
|
+
|----:&
|
48
|
+
| |----:==
|
49
|
+
| | |----:first_name
|
50
|
+
| | |
|
51
|
+
| | `----"Milton"
|
52
|
+
| |
|
53
|
+
| `----:==
|
54
|
+
| |----:last_name
|
55
|
+
| |
|
56
|
+
| `----"Waddams"
|
57
|
+
|
|
58
|
+
`----:like
|
59
|
+
|----:handle
|
60
|
+
|
|
61
|
+
`----"%milwad%"
|
62
|
+
|
63
|
+
|
64
|
+
Obviously we would never resort to building ASTs in this manner in the real world
|
65
|
+
as we would normally opt for a builder of some sort (or aquire one from a parser).
|
66
|
+
In fact, the PANDA method can be thought of as a sort of hybrid
|
67
|
+
builder/parser-less/internal DSL solution to painless AST allocation.
|
68
|
+
|
69
|
+
Here is the same AST allocated PANDA style:
|
70
|
+
|
71
|
+
# version 1
|
72
|
+
ast = Panda.build {|s| (s.first_name == 'Milton') & (s.last_name == 'Waddams') | (s.handle.like '%milwad%') }
|
73
|
+
|
74
|
+
Lets see that again in slow motion.
|
75
|
+
|
76
|
+
# version 2
|
77
|
+
ast = Panda.build {|s| s.first_name.is('Milton').and(s.last_name.is('Waddams')).or(s.handle.like('%milwad%')) }
|
78
|
+
|
79
|
+
Version 2 is identical to version 1 except that I have substituted each call to
|
80
|
+
an overloaded operator with its named (non-operator) alias (except for _like_
|
81
|
+
which has no operator version). Although it is not exactly the most elegant
|
82
|
+
looking line of code, version 2 does expose a bit of the DSL magic behind
|
83
|
+
version 1.
|
84
|
+
|
85
|
+
== The Query Object
|
86
|
+
|
87
|
+
As mentioned at the beginning, the Panda::Query class is really little more than
|
88
|
+
an immutable hash of query elements, or _clauses_, with the <tt>:where</tt>
|
89
|
+
element being the resulting AST from a Panda expression, which is passed as a
|
90
|
+
block to the constructor. Instead of being a feature-packed solution to one or
|
91
|
+
another specific domain, Panda::Query is better thought of as a flexible,
|
92
|
+
free-form base class upon which one can define their more specific query object
|
93
|
+
needs (for instance, you may not be dealing with SQL at all).
|
94
|
+
|
95
|
+
=== Examples
|
96
|
+
|
97
|
+
require 'panda_query'
|
98
|
+
|
99
|
+
q = Panda::Query.new :select => :*, :from => :leet_haxors do |lh|
|
100
|
+
(lh.handle =~ /^[Mm]atz/) | (lh.lisps.is true)
|
101
|
+
end
|
102
|
+
|
103
|
+
q.query_elements -> [:where, :from, :select]
|
104
|
+
q.select -> :*
|
105
|
+
q.from -> :leet_haxors
|
106
|
+
q.where -> (handle =~ /^[Mm]atz/ | lisps == true)
|
107
|
+
|
108
|
+
Notice that I am accessing the query elements as method calls rather than using
|
109
|
+
the [] operator. This allows me to override the behavior of certain elements
|
110
|
+
if need be, although I can also call [] on a query if I really want to. A good
|
111
|
+
example of this is Panda::Query's +order+ method which always returns an array.
|
112
|
+
|
113
|
+
# continued from above..
|
114
|
+
|
115
|
+
q[:order] -> nil
|
116
|
+
q.order -> []
|
117
|
+
|
118
|
+
q2 = q.merge :order => :last_name
|
119
|
+
|
120
|
+
q2[:order] -> :last_name
|
121
|
+
q2.order -> [:last_name]
|
122
|
+
|
123
|
+
q3 = q2.merge :order => [:last_name, :first_name]
|
124
|
+
|
125
|
+
q3.order -> [:last_name, :first_name]
|
126
|
+
|
127
|
+
IN addition to _theoretically_ being a good query object base class, Panda::Query
|
128
|
+
could also serve as a nice front-end api mechanism for some other querying
|
129
|
+
back-end interface, or for a system that uses PANDA Query as its native query
|
130
|
+
representation internally (PANDA Query was originally developed for the latter
|
131
|
+
case).
|
132
|
+
|
133
|
+
In the following example I demonstrate this idea using a hypothetical object
|
134
|
+
persistence tool with a Panda::Query based querying api.
|
135
|
+
|
136
|
+
# Lets find all the employees who are in desperate need of a raise
|
137
|
+
|
138
|
+
emps = Employee.find(:all, :order => :date_hired, :desc => true) {|e| (e.rate <= MIN_WAGE) | (e.cubicle.not nil) }
|
139
|
+
|
140
|
+
|
141
|
+
== Installation
|
142
|
+
|
143
|
+
% sudo gem install panda-query
|
144
|
+
|
145
|
+
== License[link://files/LICENSE.html]
|
146
|
+
|
147
|
+
Copyright (c) 2008 Christian Herschel Stevenson, Persapient Systems. Released
|
148
|
+
under the same license[link://files/LICENSE.html] as Ruby.
|
149
|
+
|
150
|
+
== Support
|
151
|
+
|
152
|
+
For more information, contact mailto:stevenson@persapient.com. This documentation
|
153
|
+
can be found online at http://api.persapient.com/panda_query
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
|
4
|
+
desc 'Default: Generate documentation.'
|
5
|
+
task :default => :rdoc
|
6
|
+
|
7
|
+
desc 'Generate documentation for PANDA Query.'
|
8
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
9
|
+
rdoc.rdoc_dir = 'rdoc'
|
10
|
+
rdoc.title = 'PANDA Query'
|
11
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
12
|
+
rdoc.rdoc_files.include('README', 'LICENSE', 'lib/**/*.rb')
|
13
|
+
end
|
data/lib/panda/build.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Panda
|
2
|
+
|
3
|
+
class SubjectGenerator
|
4
|
+
|
5
|
+
# Returns a new Subject whose _name_ attribute is set to _sub_name_.
|
6
|
+
#
|
7
|
+
# sg = Panda::SubjectGenerator.new
|
8
|
+
# sg[:foo] -> #<Panda::Subject:0x8260acc @name=:foo>
|
9
|
+
# sg['bar'] -> #<Panda::Subject:0x825f398 @name="bar">
|
10
|
+
#
|
11
|
+
def [](sub_name)
|
12
|
+
Subject.new sub_name
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Returns a new Subject whose _name_ attribute is set to the missing
|
18
|
+
# method's name.
|
19
|
+
#
|
20
|
+
# sg = Panda::SubjectGenerator.new
|
21
|
+
# sg.foo -> #<Panda::Subject:0x8260acc @name=:foo>
|
22
|
+
# sg.bar -> #<Panda::Subject:0x825f398 @name=:bar>
|
23
|
+
#
|
24
|
+
def method_missing(method, *args) # :doc:
|
25
|
+
Subject.new method
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Returns the result of the given block, which is passed a new SubjectGenerator
|
32
|
+
# object. If the given block does not return a Panda expression AST then a
|
33
|
+
# Panda::SyntaxError exception is raised.
|
34
|
+
#
|
35
|
+
# ast = Panda.build { |s| (s.name == 'FooHoney') | (s.booty.like 'whoa!') }
|
36
|
+
# puts ast
|
37
|
+
# puts ast.inspect
|
38
|
+
#
|
39
|
+
# _produces_:
|
40
|
+
#
|
41
|
+
# #<Panda::Boolean:0x8255ca8>
|
42
|
+
# (name == "FooHoney" | booty like "whoa!")
|
43
|
+
#
|
44
|
+
def self.build
|
45
|
+
unless Panda::Node === ast = yield(SubjectGenerator.new)
|
46
|
+
raise SyntaxError, "Invalid PANDA expression: \"#{ast}\". Are you using the != operator, perhaps?"
|
47
|
+
end
|
48
|
+
return ast
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Panda
|
2
|
+
module Expressers
|
3
|
+
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
def self.express(*args)
|
8
|
+
new.express *args
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def express(expression)
|
13
|
+
write_node str = '', expression
|
14
|
+
str
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def write_node(stream, node)
|
19
|
+
case node
|
20
|
+
when Boolean: write_boolean(stream, node)
|
21
|
+
when Comparison: write_comparison(stream, node)
|
22
|
+
else raise ArgumentError, "Bad node"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Panda::Expressers
|
2
|
+
|
3
|
+
class ActiveRecordExpresser < SqlExpresser # :nodoc:
|
4
|
+
|
5
|
+
def express(expression)
|
6
|
+
@objects = []
|
7
|
+
[ super ] + @objects
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def write_comparison(stream, node)
|
14
|
+
stream << "#{node.subject.name} #{@@operators[node.operator]} ?"
|
15
|
+
@objects << node.object
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Panda
|
2
|
+
module Expressers
|
3
|
+
|
4
|
+
|
5
|
+
class SqlExpresser < Base # :nodoc:
|
6
|
+
|
7
|
+
@@operators = Hash.new { |h,k| k.to_s.upcase }.merge(
|
8
|
+
:'&' => 'AND',
|
9
|
+
:'|' => 'OR',
|
10
|
+
:== => '=',
|
11
|
+
:not => '!='
|
12
|
+
)
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def write_boolean(stream, node)
|
17
|
+
stream << '('
|
18
|
+
write_node stream, node.left
|
19
|
+
stream << " #{@@operators[node.operator]} "
|
20
|
+
write_node stream, node.right
|
21
|
+
stream << ')'
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def write_comparison(stream, node)
|
26
|
+
stream << "#{node.subject.name} #{@@operators[node.operator]} '#{node.object}'"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'panda/expressers/base'
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module Expressers
|
5
|
+
|
6
|
+
|
7
|
+
class Inspector < Base
|
8
|
+
private
|
9
|
+
|
10
|
+
def write_boolean(stream, node)
|
11
|
+
stream << '('
|
12
|
+
write_node stream, node.left
|
13
|
+
stream << " #{node.operator} "
|
14
|
+
write_node stream, node.right
|
15
|
+
stream << ')'
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def write_comparison(stream, node)
|
20
|
+
stream << "#{node.subject.name} #{node.operator} #{node.object.inspect}"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'panda/expressers/base'
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module Expressers
|
5
|
+
|
6
|
+
|
7
|
+
class TreeExpresser < Base
|
8
|
+
|
9
|
+
def express(expression)
|
10
|
+
@indent_stk = ["\n"]
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def indent
|
18
|
+
@indent_stk.join ''
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def write_boolean(stream, node)
|
23
|
+
stream << ":#{node.operator}" << indent << '|----'
|
24
|
+
@indent_stk.push '| '
|
25
|
+
write_node stream, node.left
|
26
|
+
@indent_stk.pop
|
27
|
+
stream << indent << '|' << indent << '`----'
|
28
|
+
@indent_stk.push ' '
|
29
|
+
write_node stream, node.right
|
30
|
+
@indent_stk.pop
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def write_comparison(stream, node)
|
35
|
+
stream << ":#{node.operator}"
|
36
|
+
stream << indent << '|----'
|
37
|
+
stream << "#{node.subject.name.inspect}"
|
38
|
+
stream << indent << '|' << indent << '`----'
|
39
|
+
stream << "#{node.object.inspect}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'panda/expressers/inspector'
|
2
|
+
|
3
|
+
|
4
|
+
module Panda
|
5
|
+
|
6
|
+
class SyntaxError < ::SyntaxError
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# === The base class for all Panda tree nodes (except for the object of a comparison).
|
11
|
+
#
|
12
|
+
class Node
|
13
|
+
|
14
|
+
# Defines instance methods on the invoking class for each operator of
|
15
|
+
# _klass_. Each defined method returns a new instance of _klass_ with the
|
16
|
+
# invoking instance as the left operand, the method name as the operator,
|
17
|
+
# and the _object_ argument as the right operand. If the method's operator
|
18
|
+
# has a 'wordy' alias, the method is aliased by that name.
|
19
|
+
def self.define_operator_builders_for(klass)
|
20
|
+
klass.operators.each do |operator, op_name|
|
21
|
+
define_method operator do |object|
|
22
|
+
klass.new(self, operator, object)
|
23
|
+
end
|
24
|
+
alias_method(op_name, operator) if op_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# === The base class for binary Panda expression nodes.
|
32
|
+
#
|
33
|
+
# Expression objects have an operator (as a symbol) and left & right operands.
|
34
|
+
# Expression classes define a hash of possible operators where the key is the
|
35
|
+
# operator and the value is a 'wordy' alias for the operator which can be +nil+.
|
36
|
+
#
|
37
|
+
# There is currently no unary expression class because I have not yet figured
|
38
|
+
# out a way to implement one in a way that would be useful with this library
|
39
|
+
# (for now you'll just have to apply a bit of Demorgan to get around the absence
|
40
|
+
# of a unary negation operator).
|
41
|
+
#
|
42
|
+
class Expression < Node
|
43
|
+
@@inspector = Expressers::Inspector
|
44
|
+
|
45
|
+
attr_reader :operator, :left, :right
|
46
|
+
|
47
|
+
|
48
|
+
# Accepts a _left_ operand, an _operator_ as a symbol, and a _right_ operand.
|
49
|
+
# If _operator_ is not one of the possible operators of the invoking Epression
|
50
|
+
# class then a Panda::SyntaxError exception is raised.
|
51
|
+
def initialize(left, operator, right)
|
52
|
+
unless has_operator? operator
|
53
|
+
raise SyntaxError, "bad operator :#{operator} for #{self.class}"
|
54
|
+
end
|
55
|
+
@left, @operator, @right = left, operator, right
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Returns the receiver class's hash of possible operators where the key is
|
60
|
+
# the operator and the value is the 'wordy' alias name of the operator, or
|
61
|
+
# +nil+.
|
62
|
+
def self.operators
|
63
|
+
@operators
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Returns +true+ if the class uses the given operator.
|
68
|
+
def self.has_operator?(op)
|
69
|
+
@operators.has_key? op
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Returns the wordy alias of the operator or +nil+ if it does not have one
|
74
|
+
# or if the receiver class does not use the given operator.
|
75
|
+
def self.operator_alias_for(op)
|
76
|
+
@operators[op.to_sym]
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Sets the default expresser class for all Panda AST node's +inspect+ method.
|
81
|
+
# If +nil+ is passed then ruby's default +inspect+ method is used instead.
|
82
|
+
def self.inspector=(inspector)
|
83
|
+
@@inspector = inspector
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Returns +true+ if the receiver's class uses the given operator.
|
88
|
+
def has_operator?(op)
|
89
|
+
self.class.has_operator? op
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Returns the wordy alias name of the receiver's operator or +nil+ if it
|
94
|
+
# does not have one.
|
95
|
+
def operator_alias
|
96
|
+
self.class.operator_alias_for @operator
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Overwrites +inspect+ to use the +express+ class method of the expresser
|
101
|
+
# class assigned to <tt>@@inspector</tt> if one exists.
|
102
|
+
#
|
103
|
+
# The default expresser is Panda::Expressers::Inspector.
|
104
|
+
def inspect
|
105
|
+
@@inspector && @@inspector.express(self) || super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# === The binary boolean expression node class.
|
111
|
+
#
|
112
|
+
# The possible operators are:
|
113
|
+
# * :& (and)
|
114
|
+
# * :| (or)
|
115
|
+
#
|
116
|
+
# [NOTE:]
|
117
|
+
# The binary operators are used here because you cannot yet overload ruby's
|
118
|
+
# && and || operators <em>(at least in 1.8)</em>.
|
119
|
+
#
|
120
|
+
class Boolean < Expression
|
121
|
+
@operators = { :'&' => :and, :'|' => :or }.freeze
|
122
|
+
|
123
|
+
define_operator_builders_for self
|
124
|
+
|
125
|
+
|
126
|
+
# Same precondition as Panda::Expression plus a Panda::SyntaxError is raised
|
127
|
+
# if _left_ or _right_ is not an instance of Panda::Expression.
|
128
|
+
def initialize(left, operator, right)
|
129
|
+
unless left.kind_of?(Expression) && right.kind_of?(Expression)
|
130
|
+
raise SyntaxError, "Bad operand for #{self.class}: (#{left.inspect}) #{operator} (#{right.inspect})"
|
131
|
+
end
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# === The binary comparison expression node class.
|
139
|
+
#
|
140
|
+
# Comparison expressions consist of a Panda::Subject object as the left operand
|
141
|
+
# <em>(the 'subject' of a comparison)</em> and any type of object as the right
|
142
|
+
# operand <em>(the 'object' of a comparison)</em>.
|
143
|
+
#
|
144
|
+
# The possible operators are:
|
145
|
+
# * :== (is)
|
146
|
+
# * :not
|
147
|
+
# * :< (less_than)
|
148
|
+
# * :<= (less_than_or_equal_to)
|
149
|
+
# * :> (greater_than)
|
150
|
+
# * :>= (greater_than_or_equal_to)
|
151
|
+
# * :=~ (matches_regexp)
|
152
|
+
# * :like
|
153
|
+
# * :ilike
|
154
|
+
#
|
155
|
+
# [NOTE:]
|
156
|
+
# It is not possible in ruby 1.8 to overload the != operator so you'll
|
157
|
+
# have to call it by name (:not) :P.
|
158
|
+
#
|
159
|
+
class Comparison < Expression
|
160
|
+
@operators = {
|
161
|
+
:== => :is,
|
162
|
+
:not => nil,
|
163
|
+
:< => :less_than,
|
164
|
+
:> => :greater_than,
|
165
|
+
:<= => :less_than_or_equal_to,
|
166
|
+
:>= => :greater_than_or_equal_to,
|
167
|
+
:=~ => :matches_regexp,
|
168
|
+
:like => nil,
|
169
|
+
:ilike => nil
|
170
|
+
}.freeze
|
171
|
+
|
172
|
+
define_operator_builders_for Boolean
|
173
|
+
|
174
|
+
|
175
|
+
# Same precondition as Panda::Expression plus a Panda::SyntaxError is raised
|
176
|
+
# if _subject_ is not an instance of Panda::Subject.
|
177
|
+
def initialize(subject, operator, object)
|
178
|
+
unless subject.kind_of?(Subject)
|
179
|
+
raise SyntaxError, "Bad subject for #{self.class}: expected an instance of Panda::Subject but got #{subject.inspect}"
|
180
|
+
end
|
181
|
+
super
|
182
|
+
end
|
183
|
+
|
184
|
+
alias subject left
|
185
|
+
alias object right
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
# === The comparison subject node class.
|
191
|
+
#
|
192
|
+
# Subject nodes represent the 'subject' <em>(left operand)</em> of a Comparison
|
193
|
+
# object. Subjects are nothing more than thin wrappers around any object that
|
194
|
+
# represents the name of a comparison subject (typically a Symbol or String)
|
195
|
+
# that have Comparison operator builder methods.
|
196
|
+
class Subject < Node
|
197
|
+
attr_reader :name
|
198
|
+
define_operator_builders_for Comparison
|
199
|
+
|
200
|
+
def initialize(name)
|
201
|
+
@name = name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
data/lib/panda/query.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'panda/expressions'
|
2
|
+
require 'panda/build'
|
3
|
+
|
4
|
+
|
5
|
+
module Panda
|
6
|
+
|
7
|
+
InvalidQuery = Class.new(ArgumentError)
|
8
|
+
|
9
|
+
|
10
|
+
class Query
|
11
|
+
|
12
|
+
def initialize(args = {}, &block)
|
13
|
+
@query = (args || {}).to_hash
|
14
|
+
@query[panda_element] = Panda.build(&block) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def panda_element
|
19
|
+
:where
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
@query.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def query_elements
|
29
|
+
@query.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def [](element)
|
34
|
+
@query[element]
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def order
|
39
|
+
@order ||= [ @query[:order] || [] ].flatten
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def descends_on?(order_attr)
|
44
|
+
assert_ordered_by(order_attr) && (@descenders || load_descenders)[order_attr]
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def ascends_on?(order_attr)
|
49
|
+
!descends_on? order_attr
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def merge(query = {}, &block)
|
54
|
+
self.class.new @query.merge(query || {}), &block
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def merged_with(other = {}, &block)
|
59
|
+
self.class.new(other, &block).merge(@query)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def method_missing(method, *args)
|
66
|
+
args.empty? ? @query[method] : super
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def assert_ordered_by(attr, exception = ArgumentError)
|
71
|
+
return true if order.include?(attr)
|
72
|
+
raise exception, "query is not ordered by #{attr}"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def load_descenders
|
77
|
+
case desc = @query[:desc]
|
78
|
+
when NilClass, FalseClass: @descenders = Hash.new(false)
|
79
|
+
when TrueClass: @descenders = Hash.new(true)
|
80
|
+
else
|
81
|
+
@descenders = Hash.new(false)
|
82
|
+
[ desc ].flatten.each do |dsc|
|
83
|
+
case dsc
|
84
|
+
when Integer: @descenders[order[dsc] || raise(InvalidQuery, "query order does not include index #{dsc}")] = true
|
85
|
+
when Symbol: assert_ordered_by(dsc, InvalidQuery) && @descenders[dsc] = true
|
86
|
+
else raise InvalidQuery, "don't know how to descend on #{dsc.class} object"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@descenders
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
end
|
data/lib/panda_query.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../lib"
|
2
|
+
|
3
|
+
require 'panda/expressions'
|
4
|
+
require 'panda/build'
|
5
|
+
require 'test/unit'
|
6
|
+
|
7
|
+
|
8
|
+
class Panda::BuildTest < Test::Unit::TestCase
|
9
|
+
|
10
|
+
def test_root_oopsee
|
11
|
+
assert_raise Panda::SyntaxError do
|
12
|
+
Panda.build { |p| p.bud != nil }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def test_non_root_oopsee
|
18
|
+
assert_raise Panda::SyntaxError do
|
19
|
+
Panda.build { |p| (p.bud != nil) & (p.beer > 12) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def test_straight_abuse
|
25
|
+
assert_raise Panda::SyntaxError do
|
26
|
+
Panda.build { |p| "HI! I don't have a @$%!'n clue what I'm doing!" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../../lib"
|
2
|
+
|
3
|
+
require 'panda/expressions'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
|
7
|
+
class Panda::BooleanTest < Test::Unit::TestCase
|
8
|
+
B = Panda::Boolean
|
9
|
+
B.inspector = nil
|
10
|
+
|
11
|
+
class E < Panda::Expression
|
12
|
+
@operators = { :xor => nil }
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def test_class_operators
|
17
|
+
assert B.instance_methods.include?('&')
|
18
|
+
assert B.instance_methods.include?('|')
|
19
|
+
assert B.instance_methods.include?('and')
|
20
|
+
assert B.instance_methods.include?('or')
|
21
|
+
assert B.has_operator?(:'&')
|
22
|
+
assert B.has_operator?(:'|')
|
23
|
+
assert_same :and, B.operator_alias_for(:'&')
|
24
|
+
assert_same :or, B.operator_alias_for(:'|')
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def test_initialize
|
29
|
+
left, right = E.new(:a, :xor, :b), E.new(:x, :xor, :y)
|
30
|
+
bool = B.new left, :'&', right
|
31
|
+
assert_same left, bool.left
|
32
|
+
assert_same right, bool.right
|
33
|
+
assert_same :'&', bool.operator
|
34
|
+
|
35
|
+
assert_raise(Panda::SyntaxError) { B.new :not_exp, :'&', right }
|
36
|
+
assert_raise(Panda::SyntaxError) { B.new left, :'&', :not_exp }
|
37
|
+
assert_raise(Panda::SyntaxError) { B.new :not_exp, :'&', :not_exp }
|
38
|
+
assert_raise(Panda::SyntaxError) { B.new left, :not_op, right }
|
39
|
+
assert_raise(Panda::SyntaxError) { B.new :not_exp, :not_op, :not_exp }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../../lib"
|
2
|
+
|
3
|
+
require 'panda/expressions'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
|
7
|
+
class Panda::ComparisonTest < Test::Unit::TestCase
|
8
|
+
C = Panda::Comparison
|
9
|
+
S = Panda::Subject
|
10
|
+
|
11
|
+
|
12
|
+
def test_class_operators
|
13
|
+
{
|
14
|
+
:== => :is,
|
15
|
+
:not => nil,
|
16
|
+
:< => :less_than,
|
17
|
+
:> => :greater_than,
|
18
|
+
:<= => :less_than_or_equal_to,
|
19
|
+
:>= => :greater_than_or_equal_to,
|
20
|
+
:=~ => :matches_regexp,
|
21
|
+
:like => nil,
|
22
|
+
:ilike => nil
|
23
|
+
}.each do |op, name|
|
24
|
+
assert C.has_operator?(op)
|
25
|
+
assert_same name, C.operator_alias_for(op)
|
26
|
+
end
|
27
|
+
assert C.instance_methods.include?('&')
|
28
|
+
assert C.instance_methods.include?('|')
|
29
|
+
assert C.instance_methods.include?('and')
|
30
|
+
assert C.instance_methods.include?('or')
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def test_initialize
|
35
|
+
sub = S.new(:ass)
|
36
|
+
comp = C.new sub, :like, 'whoa!'
|
37
|
+
assert_same sub, comp.subject
|
38
|
+
assert_same comp.subject, comp.left
|
39
|
+
assert_equal 'whoa!', comp.object
|
40
|
+
assert_same comp.object, comp.right
|
41
|
+
assert_same :like, comp.operator
|
42
|
+
|
43
|
+
assert_raise(Panda::SyntaxError) { C.new :not_sub, :==, Object.new }
|
44
|
+
assert_raise(Panda::SyntaxError) { C.new sub, :not_op, Object.new }
|
45
|
+
assert_raise(Panda::SyntaxError) { C.new :not_sub, :not_op, Object.new }
|
46
|
+
assert_nothing_raised { C.new sub, :not, nil }
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../../lib"
|
2
|
+
|
3
|
+
require 'panda/expressions'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
|
7
|
+
class Panda::ExpressionTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
class Foo < Panda::Expression
|
10
|
+
@operators = { :'|' => :bar, :do => nil }.freeze
|
11
|
+
define_operator_builders_for self
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def test_class_define_operator_builders_for
|
16
|
+
assert Foo.instance_methods.include?('|')
|
17
|
+
assert Foo.instance_methods.include?('bar')
|
18
|
+
assert Foo.instance_methods.include?('do')
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def test_class_operators
|
23
|
+
assert_equal({ :'|' => :bar, :do => nil }, Foo.operators)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def test_class_has_operator?
|
28
|
+
assert Foo.has_operator?(:'|')
|
29
|
+
assert Foo.has_operator?(:do)
|
30
|
+
assert !Foo.has_operator?(:phoo)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def test_class_operator_alias_for
|
35
|
+
assert_same :bar, Foo.operator_alias_for(:'|')
|
36
|
+
assert_nil Foo.operator_alias_for(:do)
|
37
|
+
assert_nil Foo.operator_alias_for(:phoo)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def test_initialize
|
42
|
+
foo = Foo.new :left, :'|', :right
|
43
|
+
assert_same :left, foo.left
|
44
|
+
assert_same :'|', foo.operator
|
45
|
+
assert_same :right, foo.right
|
46
|
+
assert_raise(Panda::SyntaxError) { Foo.new 7, :==, 7 }
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def test_has_operator?
|
51
|
+
foo = Foo.new :left, :'|', :right
|
52
|
+
assert foo.has_operator?(:'|')
|
53
|
+
assert foo.has_operator?(:do)
|
54
|
+
assert !foo.has_operator?(:phoo)
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def test_operator_alias
|
59
|
+
assert_same :bar, Foo.new(:left, :'|', :right).operator_alias
|
60
|
+
assert_nil Foo.new(:left, :do, :right).operator_alias
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../../lib"
|
2
|
+
|
3
|
+
require 'panda/expressions'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
|
7
|
+
class Panda::SubjectTest < Test::Unit::TestCase
|
8
|
+
S = Panda::Subject
|
9
|
+
|
10
|
+
|
11
|
+
def test_class_operators
|
12
|
+
{
|
13
|
+
:== => :is,
|
14
|
+
:not => nil,
|
15
|
+
:< => :less_than,
|
16
|
+
:> => :greater_than,
|
17
|
+
:<= => :less_than_or_equal_to,
|
18
|
+
:>= => :greater_than_or_equal_to,
|
19
|
+
:=~ => :matches_regexp,
|
20
|
+
:like => nil,
|
21
|
+
:ilike => nil
|
22
|
+
}.each do |op, name|
|
23
|
+
assert S.instance_methods.include?(op.to_s)
|
24
|
+
assert name.nil? || S.instance_methods.include?(name.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def test_initialize
|
30
|
+
sub = S.new(:bob)
|
31
|
+
assert_same :bob, sub.name
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
$:.unshift "#{File.dirname(__FILE__)}/../../lib"
|
2
|
+
|
3
|
+
require 'panda/query'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
|
7
|
+
class Panda::QueryTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
Q = Panda::Query
|
10
|
+
|
11
|
+
class Fq < Panda::Query
|
12
|
+
def panda_element
|
13
|
+
:filter
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def test_initialize
|
19
|
+
q = Q.new(:where => Panda.build { |o| (o.mark == 7) & (o.explodes.not true) }) { |o| o.mark >= 7 }
|
20
|
+
assert_kind_of Panda::Comparison, q.where
|
21
|
+
q2 = Q.new q
|
22
|
+
assert_equal q.instance_variable_get(:@query), q2.instance_variable_get(:@query)
|
23
|
+
assert_not_same q.instance_variable_get(:@query), q2.instance_variable_get(:@query)
|
24
|
+
assert_equal({}, Q.new(nil).to_hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def test_panda_element_overide
|
29
|
+
fq = Fq.new { |o| (o.mark == 7) & (o.explodes.not true) }
|
30
|
+
assert_nil fq.where
|
31
|
+
assert_kind_of Panda::Boolean, fq.filter
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def test_to_hash
|
36
|
+
q = Q.new :key => :value
|
37
|
+
assert_equal q.instance_variable_get(:@query), q.to_hash
|
38
|
+
assert_not_same q.instance_variable_get(:@query), q.to_hash
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def test_query_elements
|
43
|
+
q = Q.new(:limit => 7, :offset => 666) {|o| o.exists.not true }
|
44
|
+
assert [:limit, :offset, :where].all? {|e| q.query_elements.include? e }
|
45
|
+
assert_equal 3, q.query_elements.size
|
46
|
+
assert Q.new.query_elements.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def test_index_op
|
51
|
+
assert_same :bob, Q.new(:order => :bob)[:order]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def test_order
|
56
|
+
assert_equal [:obsolete], Q.new(:order => :obsolete).order
|
57
|
+
assert_equal [:first, :last], Q.new(:order => [:first, :last]).order
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def test_ascends_on_descends_on
|
62
|
+
assert_descenders Q.new(:order => [:x, :y, :z])
|
63
|
+
|
64
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => false)
|
65
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => true), :x => true, :y => true, :z => true
|
66
|
+
|
67
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => :x), :x => true
|
68
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => 2), :z => true
|
69
|
+
|
70
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => [:y, :z]), :y => true, :z => true
|
71
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => [0, 1]), :x => true, :y => true
|
72
|
+
assert_descenders Q.new(:order => [:x, :y, :z], :desc => [0, :y, 2]), :x => true, :y => true, :z => true
|
73
|
+
|
74
|
+
assert_raise(ArgumentError) { Q.new(:order => :x).descends_on?(:y) }
|
75
|
+
assert_raise(ArgumentError) { Q.new(:order => :x).ascends_on?(:y) }
|
76
|
+
|
77
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => :z).descends_on?(:x) }
|
78
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => :z).ascends_on?(:x) }
|
79
|
+
|
80
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => 7).descends_on?(:x) }
|
81
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => 7).ascends_on?(:x) }
|
82
|
+
|
83
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => {}).descends_on?(:x) }
|
84
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y], :desc => 1.21).ascends_on?(:x) }
|
85
|
+
|
86
|
+
assert_nothing_raised { Q.new(:order => [:x, :y], :desc => :z) }
|
87
|
+
assert_nothing_raised { Q.new(:order => [:x, :y], :desc => 7) }
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def test_merge
|
92
|
+
q1 = Q.new(:order => :x) { |o| o.x.not 666 }
|
93
|
+
q2 = q1.merge(:limit => 7) { |o| (o.x <= 665) & (o.x >= 667) }
|
94
|
+
assert_kind_of Panda::Query, q2
|
95
|
+
assert_same 7, q2[:limit]
|
96
|
+
assert_kind_of Panda::Boolean, q2[:where]
|
97
|
+
|
98
|
+
q3 = q1.merge q2
|
99
|
+
assert_kind_of Panda::Query, q3
|
100
|
+
assert_equal q2.to_hash, q3.to_hash
|
101
|
+
|
102
|
+
q3 = q1.merge(q2) { |oh| oh.why.not nil }
|
103
|
+
assert_kind_of Panda::Query, q3
|
104
|
+
assert_not_same q2[:where], q3[:where]
|
105
|
+
|
106
|
+
q4 = nil
|
107
|
+
assert_nothing_raised { q4 = q3.merge nil }
|
108
|
+
assert_kind_of Panda::Query, q4
|
109
|
+
assert_equal q3.to_hash, q4.to_hash
|
110
|
+
|
111
|
+
assert Q.new(:order => [:x, :y, :z], :desc => true).merge(:order => [:i, :j, :k]).descends_on?(:k)
|
112
|
+
assert Q.new(:order => [:x, :y, :z], :desc => 0).merge(:order => [:y, :z]).descends_on?(:y)
|
113
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:order => [:x, :y, :z], :desc => :x).merge(:order => [:y, :z]).descends_on?(:y) }
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def test_merged_with
|
118
|
+
q1 = Q.new(:order => :x) { |o| o.x.not 666 }
|
119
|
+
q2 = q1.merged_with(:limit => 7) { |o| (o.x <= 665) & (o.x >= 667) }
|
120
|
+
assert_kind_of Panda::Query, q2
|
121
|
+
assert_same 7, q2[:limit]
|
122
|
+
assert_kind_of Panda::Comparison, q2[:where]
|
123
|
+
|
124
|
+
q3 = q1.merged_with q2
|
125
|
+
assert_kind_of Panda::Query, q3
|
126
|
+
assert_equal q2.to_hash, q3.to_hash
|
127
|
+
|
128
|
+
q3 = q1.merged_with(q2) { |oh| oh.why.not nil }
|
129
|
+
assert_kind_of Panda::Query, q3
|
130
|
+
assert_same q2[:where], q3[:where]
|
131
|
+
|
132
|
+
q4 = nil
|
133
|
+
assert_nothing_raised { q4 = q3.merged_with nil }
|
134
|
+
assert_kind_of Panda::Query, q4
|
135
|
+
assert_equal q3.to_hash, q4.to_hash
|
136
|
+
|
137
|
+
assert Q.new(:desc => true).merged_with(:order => [:i, :j, :k]).descends_on?(:k)
|
138
|
+
assert Q.new(:desc => 0).merged_with(:order => [:y, :z]).descends_on?(:y)
|
139
|
+
assert_raise(Panda::InvalidQuery) { Q.new(:desc => :x).merged_with(:order => [:y, :z]).descends_on?(:y) }
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def assert_descenders(q, descends = {})
|
146
|
+
q.order.each do |elem|
|
147
|
+
if descends[elem]
|
148
|
+
assert q.descends_on?(elem) && !q.ascends_on?(elem)
|
149
|
+
else
|
150
|
+
assert q.ascends_on?(elem) && !q.descends_on?(elem)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
data/test/panda_suite.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: panda_query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hersch Stevenson (xian)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-09-30 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: stevenson@persapient.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- README
|
27
|
+
- LICENSE
|
28
|
+
- Rakefile
|
29
|
+
- lib/panda/core_ext/hash.rb
|
30
|
+
- lib/panda/expressers/examples/sql_expresser.rb
|
31
|
+
- lib/panda/expressers/examples/active_record_expresser.rb
|
32
|
+
- lib/panda/expressers/tree_expresser.rb
|
33
|
+
- lib/panda/expressers/inspector.rb
|
34
|
+
- lib/panda/expressers/base.rb
|
35
|
+
- lib/panda/build.rb
|
36
|
+
- lib/panda/expressions.rb
|
37
|
+
- lib/panda/query.rb
|
38
|
+
- lib/panda_query.rb
|
39
|
+
- test/panda/expressions/comparison_case.rb
|
40
|
+
- test/panda/expressions/boolean_case.rb
|
41
|
+
- test/panda/expressions/expression_case.rb
|
42
|
+
- test/panda/expressions/subject_case.rb
|
43
|
+
- test/panda/query_case.rb
|
44
|
+
- test/panda/expressions_suite.rb
|
45
|
+
- test/panda/build_case.rb
|
46
|
+
- test/panda_suite.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://api.persapient.com/panda_query
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: panda-query
|
69
|
+
rubygems_version: 1.0.1
|
70
|
+
signing_key:
|
71
|
+
specification_version: 2
|
72
|
+
summary: A small library for building generic query objects.
|
73
|
+
test_files:
|
74
|
+
- test/panda_suite.rb
|