panda_query 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|