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