PANDA Query
The PANDA Query library provides a generic query object, which is basically an immutable hash whose where clause is represented as a boolean/comparison expression AST, or PANDA (Painlessly Allocated via “Native DSL” AST).
The Goal of PANDA is to make it possible to build where style expression ASTs ‘painlessly’ on the fly (in a single line of code) that are highly readable, with code that closely resembles the expression being built. This is accomplished with a kind of internal (or ‘native’) DSL implemented in pure ruby via method calls on Panda objects.
PANDA is basically a variation of the Parser-less Interpreter/Internal DSL patterns as described in Russ Olsen’s Design Patterns in Ruby.
The Panda AST
In order to fully grasp the painlessness of building a PANDA, it would first be wise to see the painful example first.
In the following example I build a PANDA node by node the hard way in order to provide a little insight on the relatively simple anatomy of a Panda tree. The statement I build here would be the SQL where clause equivalent to “first_name = 'Milton' AND last_name = 'Waddams' OR handle LIKE '%milwad%'
”.
require 'panda/expressions'
s1 = Panda::Subject.new :first_name
c1 = Panda::Comparison.new s1, :==, 'Milton'
s2 = Panda::Subject.new :last_name
c2 = Panda::Comparison.new s2, :==, 'Waddams'
b1 = Panda::Boolean.new c1, :'&', c2
s3 = Panda::Subject.new :handle
c3 = Panda::Comparison.new s3, :like, '%milwad%'
root = Panda::Boolean.new b1, :'|', c3
To get a better look at what was just built, we can execute the following code:
require 'panda/expressers/tree_expresser'
puts Panda::Expressers::TreeExpresser.express(root) # puts the following..
:|
|----:&
| |----:==
| | |----:first_name
| | |
| | `----"Milton"
| |
| `----:==
| |----:last_name
| |
| `----"Waddams"
|
`----:like
|----:handle
|
`----"%milwad%"
Obviously we would never resort to building ASTs in this manner in the real world as we would normally opt for a builder of some sort (or aquire one from a parser). In fact, the PANDA method can be thought of as a sort of hybrid builder/parser-less/internal DSL solution to painless AST allocation.
Here is the same AST allocated PANDA style:
# version 1
ast = Panda.build {|s| (s.first_name == 'Milton') & (s.last_name == 'Waddams') | (s.handle.like '%milwad%') }
Lets see that again in slow motion.
# version 2
ast = Panda.build {|s| s.first_name.is('Milton').and(s.last_name.is('Waddams')).or(s.handle.like('%milwad%')) }
Version 2 is identical to version 1 except that I have substituted each call to an overloaded operator with its named (non-operator) alias (except for like which has no operator version). Although it is not exactly the most elegant looking line of code, version 2 does expose a bit of the DSL magic behind version 1.
The Query Object
As mentioned at the beginning, the Panda::Query class is really little more than an immutable hash of query elements, or clauses, with the :where
element being the resulting AST from a Panda expression, which is passed as a block to the constructor. Instead of being a feature-packed solution to one or another specific domain, Panda::Query is better thought of as a flexible, free-form base class upon which one can define their more specific query object needs (for instance, you may not be dealing with SQL at all).
Examples
require 'panda_query'
q = Panda::Query.new :select => :*, :from => :leet_haxors do |lh|
(lh.handle =~ /^[Mm]atz/) | (lh.lisps.is true)
end
q.query_elements -> [:where, :from, :select]
q.select -> :*
q.from -> :leet_haxors
q.where -> (handle =~ /^[Mm]atz/ | lisps == true)
Notice that I am accessing the query elements as method calls rather than using the [] operator. This allows me to override the behavior of certain elements if need be, although I can also call [] on a query if I really want to. A good example of this is Panda::Query’s order
method which always returns an array.
# continued from above..
q[:order] -> nil
q.order -> []
q2 = q.merge :order => :last_name
q2[:order] -> :last_name
q2.order -> [:last_name]
q3 = q2.merge :order => [:last_name, :first_name]
q3.order -> [:last_name, :first_name]
IN addition to theoretically being a good query object base class, Panda::Query could also serve as a nice front-end api mechanism for some other querying back-end interface, or for a system that uses PANDA Query as its native query representation internally (PANDA Query was originally developed for the latter case).
In the following example I demonstrate this idea using a hypothetical object persistence tool with a Panda::Query based querying api.
# Lets find all the employees who are in desperate need of a raise
emps = Employee.find(:all, :order => :date_hired, :desc => true) {|e| (e.rate <= MIN_WAGE) | (e.cubicle.not nil) }
Installation
% sudo gem install panda-query
License
Copyright © 2008 Christian Herschel Stevenson, Persapient Systems. Released under the same license as Ruby.
Support
For more information, contact [email protected]. This documentation can be found online at api.persapient.com/panda_query