panda_query 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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,7 @@
1
+ class Hash
2
+
3
+ def merged_with(other, &block)
4
+ (other || {}).merge self, &block
5
+ end
6
+
7
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'panda/core_ext/hash'
2
+ require 'panda/expressers/tree_expresser'
3
+ require 'panda/query'
@@ -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,7 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}/expressions"
2
+
3
+ require 'test/unit'
4
+ require 'expression_case'
5
+ require 'boolean_case'
6
+ require 'comparison_case'
7
+ require 'subject_case'
@@ -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
@@ -0,0 +1,6 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}/panda"
2
+
3
+ require 'test/unit'
4
+ require 'expressions_suite'
5
+ require 'build_case'
6
+ require 'query_case'
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