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