[Dbix-class] [OT][ANNOUNCE] SQL Generation with SQL::DB

Emanuele Zeppieri ema_zep at libero.it
Mon Sep 10 16:20:34 GMT 2007


Mark Lawrence wrote:

>> So I misunderstood before, but that's probably even more limited than 
>> expected.
> 
> Care to explain why you think so?

Yes of course.

First of all sorry for my late.

Then you're perfectly right Mark, I've been way too vague, cryptic and 
confused (it was also because I tried to be brief, while it probably 
made you waste even more time. I apologize also for that).

So please let me explain in this message what I meant in my previous 
messages, step by step (through some examples), and let me also show my 
conclusions on SQL::DB (after a deeper examination).

These were my points (more or less in chronological order from message one).

1. No gain in expressiveness in the dynamic case
(Over SQL::Abstract)
------------------------------------------------

You show that the equivalent of the following SQL::Abstract paradigm 
(showed by Demi Laurent):

>   foreach my $field ($form->param) {
>     my @vals = $form->param($field) or next;
>     $search_criteria{$field} = @vals > 1 ? {-in => \@vals} : $vals[0];
>   }

in SQL::DB would be:

>     foreach my $field ($form->param) {
>         my @vals = $form->param($field) or next;
>         $expr    = $expr & ( @vals > 1 ? $table->$field->in(@vals)
>                                        : $table->$field == $vals[0] );
>     }

This is admittedly not that dramatic, but I don't see any significant 
gain in expressiveness over SQL::Abstract.

So the only situation I can see where an SQL::DB where clause offers 
more expressiveness over a Perl data structure, is when such clause is 
(fully) known at design time.
But in this case I would simply put my queries in some catalog (as they 
are known): that's why I think the /static/ case is not really relevant 
(as i said in my first message).

So let's examine some more /dynamic/ examples, where the SQL::Abstract 
advantage should be more evident.

2. More dynamic cases
---------------------

Let's suppose to have a form which permits the user to enter not only 
the field values, but also the relational (comparison) operators through 
which they must be compared (so for each field the comparison operator 
is not known until runtime).

The RT query builder is a perfect example of that:
https://rt.cpan.org/Search/Build.html
(login requested)

For clarity and simplicity, let's assume our program gets a data 
structure filled like this (the M$ Access form filter is another example 
of this; it would permit to put in the search fields exactly the strings 
used as values in the following hash - more on that later):

$form = {
     name   => '= Mark',
     age    => '<= 30' ,
     height => '> 180' ,
     weight => '< 80'
};

which is intended to produce the following SQL where clause:

(name = 'Mark') AND (age <= 30) AND (height > 180) AND (weight < 80)

To get this, through SQL::Abstract we can simply do:

my %abstr_where
     = map { $_ => { split /\s+/, $form->{$_} } } keys %{ $form };

How would you that with SQL::DB?
The easiest thing I can think of is an hash with all the possible 
relational operators as keys and as values the refs to the subs which 
execute the actual subexpressions.
It seems a significant additional work for the user.

Also, this is one of the situations where simply concatenating the hash 
values into your expression could help (just to explain why I did 
mention that - more on that later).

Now a slightly more complicated example.
Let's assume our form could be like this:

my $form = {
     name   => 'Mark',             # '=' is implicit!
     age    => '<30   > 60   =40',
     height => ' > 180 '           # spaces are optional anywhere.
};

which is intended to produce:

( (age < 30) OR (age > 60) OR (age = 40) ) AND (height > 180)
AND (name = 'Mark')

This is again along the syntax permitted by the above mentioned form 
filter function in M$ Access.

With SQL::Abstract it would be something like:

my $rel_op = qr/<=|>=|<|>|=/; # Let's limit to that.
my %abstr_where;
while ( my ($field, $cond) = each %{ $form } ) {
     my @atomic_conds = grep /\S/, split /($rel_op|\s+)/, $cond;
     if ( @atomic_conds == 1 ) {
         $atomic_conds[1] = $atomic_conds[0]; $atomic_conds[0] = '='
     }
     my @abstr_predicate;
     push @abstr_predicate, { shift @atomic_conds, shift @atomic_conds }
         while @atomic_conds;
     $abstr_where{$field} = ['-or', @abstr_predicate]
}

(Note that we've had to take care for the implicit '=' operator and for 
the optional spaces between the operator and the value and around the 
predicate, to reproduce the fuzziness of the Access form filter).

Well, this last example was really unnecessary, since the delta in 
difficulty between SQL::Abstract and SQL::DB should be about the same as 
the previous example, but it was to show you another example where 
simple string manipulations could help, and to introduce the next point.

3. An even more dynamic case
----------------------------

Really, the Access form filter permits to fill the field values with 
arbitrarily complex expressions, like this:

my $form = {
     age    => '( >30 OR name=Frank ) AND ( <60 OR height>180 )',
     weight => 80
};

Here the string manipulations would help a lot: you could just slightly 
pre-process the expressions and concatenate them into the full 
expression (if you didn't care of the fragility: more on that later).

Well, since this was the only situation I saw where SQL::DB could have a 
possible advantage over SQL::Abstract (which is useless here), I thought 
SQL::DB was aimed exactly at that, that's why I insisted on string 
manipulations.
At first I thought your module was just aimed to match such situations, 
where you get from the outside structured data together with ready made 
expressions (and that's why I was warning you against string 
manipulations ;-)

(It may seem a contrived example, but I'm fully involved with that, 
since I'm writing right now (for work) a module to reproduce the Access 
form filter (with even some more magic (which is called SQL::WateryPoo, 
for  a reason ;-)
As a side note, I had to write a full boolean expressions parser (with 
Parse::Yapp) since anything quicker resulted way too fragile. Just to 
remark that I'm not supporting simple string manipulations at all ;-)

4. Dynamic query manipulation.
------------------------------

Let's go back to a more common situation, where again IMHO SQL::Abstract 
has a significant advantage over SQL::DB (even greater than with the 
examples in points 1 and 2 before).

Let's assume you permit the user to perform some manipulations of the 
where clause he's creating, such as add a predicate at a time, remove 
any predicate, move predicates up/down, nest predicates (and un-nest 
them), change the topmost logical aggregator etc.

Again, this is exactly what the above mentioned RT query builder 
permits: look at the "Current search" block on the right (of course it's 
server side Perl, not javascript).

Well, with SQL::Abstract this would be trivial: it's just sufficient to 
manage an array.

Let's assume we start with the following array (kept server-side):

my @tmp_abstr_where = (
     'name'   => {'=' , 'Mark'}, # predicate 0
     'age'    => {'<=', 30    }, # predicate 1
     'height' => {'>=', 180   }, # ...
     'weight' => {'<' , 80    }
);

adding another predicate it's just a matter of a push()/unshift().

Removing a predicate (given the /predicate index/ in $i) it's just:

splice @tmp_abstr_where, $i*2, 2;

Swapping two consecutive predicates (given the index $i of the first 
one), to move them up/down:

my $i2 = $i * 2;
( @tmp_abstr_where[$i2, $i2+1], @tmp_abstr_where[$i2+2, $i2+3] )
     = ( @tmp_abstr_where[$i2+2, $i2+3], @tmp_abstr_where[$i2, $i2+1] );

Nesting two consecutive predicates (given the index $i of the first one):

my $op = '-or'; # For example.
splice @tmp_abstr_where, $i*2, 4,
     ( $op, [ @tmp_abstr_where[$i*2..$i*2+3] ] );

Un-nesting them back:

splice @tmp_abstr_where, $i*2, 2, @{ $tmp_abstr_where[$i*2+1] };

Then, when you've done, you simply do:

my $op = '-and'; # For example.
@abstr_where = ( $op => \@tmp_abstr_where );

($stmt, @bind) = SQL::Abstract->new->where(\@abstr_where);

And changing the topmost logical aggregator is just:

my $op = $new_op;
@abstr_where = ( $op => \@tmp_abstr_where );

Well, you got the idea (more sophisticated manipulations are just as easy).

Now I see no easy way to do anything like this with SQL::DB (except for 
adding a new predicate).

Granted, you could manage an array where you put the atomic predicates, 
to assemble them together at the end, but if you end up with a nested 
structure like this:

-and => [
     ...
     -or => [
         ...
         -and => [
             ...
             -or => [...]
             -or => [...]
         ]
     ]
]

you should parse/visit the structure to evaluate the subexpressions in 
the correct order (whereas SQL::Abstract does this for you).

This is exactly what I meant when I asked you:

> If you have both &'s and |'s in your (full) expression,
> how can you find the correct order to evaluate the various subexpressions?

It could seem unfair to use a data structure which matches a compatible 
SQL::Abstract data structure, but that's what you naturally end up if 
you want to keep an /abstract/ representation of your queries.

Using an abstract representation like that would also permit you to 
easily serialize/save them, retrieve them later and let the user 
manipulate them etc.

In a word, it seems that SQL::DB is not very good to offer on abstract 
representation of the queries, IMHO.

(As a side note, at work I've developed a query builder that permits the 
above mentioned operations by the user and much more. For that, even 
SQL::Abstract wasn't enough, so I wrote my own module 
(SQL::SimpleAbstraction, not ready for public consuption yet, but it 
*will be* open-sourced if someone is interested).
It's even less expressive than SQL::Abstract (it only uses dumb nested 
arrays and a lengthy hash for the atomic predicate), but it permits such 
  manipulations very easily (they are already in the API).

5. Conclusions
--------------

SQL::DB is cute, is very perlish and it is written in a very clean and 
structured way (both the docs and the code), and I've certainly greatly 
underestimated it (I apologize for that) and I largely missed its point.

Nonetheless, I don't see any significant advantage over SQL::Abstract, 
or rather, I see only disadvantages (albeit slight in the most common 
cases), as shown in points 1, 2 and 4 above.

Yet, it has a broader scope than SQL::Abstract and space to evolve.

Cheers,
Emanuele.



More information about the DBIx-Class mailing list