X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FSQL%2FAbstract.pm;h=ad1772261be577438df9fb1b0c62bd43857647bf;hb=f267b646207361aa110bff7df661b5881b5c8319;hp=c0de4d5b6950a2f465f8ef02c4b73a7d0cbe19b4;hpb=f2d5020d799a39cecb1f165a75b15b793a0d3453;p=scpubgit%2FQ-Branch.git diff --git a/lib/SQL/Abstract.pm b/lib/SQL/Abstract.pm index c0de4d5..ad17722 100644 --- a/lib/SQL/Abstract.pm +++ b/lib/SQL/Abstract.pm @@ -15,7 +15,7 @@ use Scalar::Util qw/blessed/; # GLOBALS #====================================================================== -our $VERSION = '1.50'; +our $VERSION = '1.55'; # This would confuse some packagers #$VERSION = eval $VERSION; # numify for warning-free dev releases @@ -25,8 +25,8 @@ our $AUTOLOAD; # special operators (-in, -between). May be extended/overridden by user. # See section WHERE: BUILTIN SPECIAL OPERATORS below for implementation my @BUILTIN_SPECIAL_OPS = ( - {regex => qr/^(not )?between$/i, handler => \&_where_field_BETWEEN}, - {regex => qr/^(not )?in$/i, handler => \&_where_field_IN}, + {regex => qr/^(not )?between$/i, handler => '_where_field_BETWEEN'}, + {regex => qr/^(not )?in$/i, handler => '_where_field_IN'}, ); #====================================================================== @@ -63,7 +63,7 @@ sub new { delete $opt{case} if $opt{case} && $opt{case} ne 'lower'; # default logic for interpreting arrayrefs - $opt{logic} = uc $opt{logic} || 'OR'; + $opt{logic} = $opt{logic} ? uc $opt{logic} : 'OR'; # how to return bind vars # LDNOTE: changed nwiger code : why this 'delete' ?? @@ -422,7 +422,6 @@ sub _where_HASHREF { my ($self, $where) = @_; my (@sql_clauses, @all_bind); - # LDNOTE : don't really know why we need to sort keys for my $k (sort keys %$where) { my $v = $where->{$k}; @@ -442,11 +441,17 @@ sub _where_HASHREF { sub _where_op_in_hash { - my ($self, $op, $v) = @_; + my ($self, $op_str, $v) = @_; + + $op_str =~ /^ (AND|OR|NEST) ( \_? \d* ) $/xi + or puke "unknown operator: -$op_str"; + + my $op = uc($1); # uppercase, remove trailing digits + if ($2) { + belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. ' + . "You probably wanted ...-and => [ $op_str => COND1, $op_str => COND2 ... ]"; + } - $op =~ /^(AND|OR|NEST)[_\d]*/i - or puke "unknown operator: -$op"; - $op = uc($1); # uppercase, remove trailing digits $self->_debug("OP(-$op) within hashref, recursing..."); $self->_SWITCH_refkind($v, { @@ -457,7 +462,7 @@ sub _where_op_in_hash { HASHREF => sub { if ($op eq 'OR') { - return $self->_where_ARRAYREF([%$v], 'OR'); + return $self->_where_ARRAYREF([ map { $_ => $v->{$_} } (sort keys %$v) ], 'OR'); } else { # NEST | AND return $self->_where_HASHREF($v); @@ -499,11 +504,18 @@ sub _where_hashpair_ARRAYREF { $self->_debug("ARRAY($k) means distribute over elements"); # put apart first element if it is an operator (-and, -or) - my $op = $v[0] =~ /^-/ ? shift @v : undef; - $self->_debug("OP($op) reinjected into the distributed array") if $op; - + my $op = ( + (defined $v[0] && $v[0] =~ /^ - (?: AND|OR ) $/ix) + ? shift @v + : '' + ); my @distributed = map { {$k => $_} } @v; - unshift @distributed, $op if $op; + + if ($op) { + $self->_debug("OP($op) reinjected into the distributed array"); + unshift @distributed, $op; + } + my $logic = $op ? substr($op, 1) : ''; return $self->_recurse_where(\@distributed, $logic); @@ -516,9 +528,10 @@ sub _where_hashpair_ARRAYREF { } sub _where_hashpair_HASHREF { - my ($self, $k, $v) = @_; + my ($self, $k, $v, $logic) = @_; + $logic ||= 'and'; - my (@all_sql, @all_bind); + my ($all_sql, @all_bind); for my $op (sort keys %$v) { my $val = $v->{$op}; @@ -535,7 +548,19 @@ sub _where_hashpair_HASHREF { # CASE: special operators like -in or -between my $special_op = first {$op =~ $_->{regex}} @{$self->{special_ops}}; if ($special_op) { - ($sql, @bind) = $special_op->{handler}->($self, $k, $op, $val); + my $handler = $special_op->{handler}; + if (! $handler) { + puke "No handler supplied for special operator matching $special_op->{regex}"; + } + elsif (not ref $handler) { + ($sql, @bind) = $self->$handler ($k, $op, $val); + } + elsif (ref $handler eq 'CODE') { + ($sql, @bind) = $handler->($self, $k, $op, $val); + } + else { + puke "Illegal handler for special operator matching $special_op->{regex} - expecting a method name or a coderef"; + } } else { $self->_SWITCH_refkind($val, { @@ -559,6 +584,10 @@ sub _where_hashpair_HASHREF { @bind = @sub_bind; }, + HASHREF => sub { + ($sql, @bind) = $self->_where_hashpair_HASHREF($k, $val, $op); + }, + UNDEF => sub { # CASE: col => {op => undef} : sql "IS (NOT)? NULL" my $is = ($op =~ $self->{equality_op}) ? 'is' : ($op =~ $self->{inequality_op}) ? 'is not' : @@ -575,11 +604,10 @@ sub _where_hashpair_HASHREF { }); } - push @all_sql, $sql; + ($all_sql) = (defined $all_sql and $all_sql) ? $self->_join_sql_clauses($logic, [$all_sql, $sql], []) : $sql; push @all_bind, @bind; } - - return $self->_join_sql_clauses('and', \@all_sql, \@all_bind); + return ($all_sql, @all_bind); } @@ -587,20 +615,30 @@ sub _where_hashpair_HASHREF { sub _where_field_op_ARRAYREF { my ($self, $k, $op, $vals) = @_; - if(@$vals) { - $self->_debug("ARRAY($vals) means multiple elements: [ @$vals ]"); + my @vals = @$vals; #always work on a copy + + if(@vals) { + $self->_debug("ARRAY($vals) means multiple elements: [ @vals ]"); + + # see if the first element is an -and/-or op + my $logic; + if ($vals[0] =~ /^ - ( AND|OR ) $/ix) { + $logic = uc $1; + shift @vals; + } + + # distribute $op over each remaining member of @vals, append logic if exists + return $self->_recurse_where([map { {$k => {$op, $_}} } @vals], $logic); # LDNOTE : had planned to change the distribution logic when # $op =~ $self->{inequality_op}, because of Morgan laws : # with {field => {'!=' => [22, 33]}}, it would be ridiculous to generate # WHERE field != 22 OR field != 33 : the user probably means # WHERE field != 22 AND field != 33. - # To do this, replace the line below by : + # To do this, replace the above to roughly : # my $logic = ($op =~ $self->{inequality_op}) ? 'AND' : 'OR'; - # return $self->_recurse_where([map { {$k => {$op, $_}} } @$vals], $logic); + # return $self->_recurse_where([map { {$k => {$op, $_}} } @vals], $logic); - # distribute $op over each member of @$vals - return $self->_recurse_where([map { {$k => {$op, $_}} } @$vals]); } else { # try to DWIM on equality operators @@ -687,16 +725,39 @@ sub _where_UNDEF { sub _where_field_BETWEEN { my ($self, $k, $op, $vals) = @_; - ref $vals eq 'ARRAY' && @$vals == 2 - or puke "special op 'between' requires an arrayref of two values"; + (ref $vals eq 'ARRAY' && @$vals == 2) or + (ref $vals eq 'REF' && (@$$vals == 1 || @$$vals == 2 || @$$vals == 3)) + or puke "special op 'between' requires an arrayref of two values (or a scalarref or arrayrefref for literal SQL)"; - my ($label) = $self->_convert($self->_quote($k)); - my ($placeholder) = $self->_convert('?'); - my $and = $self->_sqlcase('and'); + my ($clause, @bind, $label, $and, $placeholder); + $label = $self->_convert($self->_quote($k)); + $and = ' ' . $self->_sqlcase('and') . ' '; + $placeholder = $self->_convert('?'); $op = $self->_sqlcase($op); - my $sql = "( $label $op $placeholder $and $placeholder )"; - my @bind = $self->_bindtype($k, @$vals); + if (ref $vals eq 'REF') { + ($clause, @bind) = @$$vals; + } + else { + my (@all_sql, @all_bind); + + foreach my $val (@$vals) { + my ($sql, @bind) = $self->_SWITCH_refkind($val, { + SCALAR => sub { + return ($placeholder, ($val)); + }, + SCALARREF => sub { + return ($self->_convert($$val), ()); + }, + }); + push @all_sql, $sql; + push @all_bind, @bind; + } + + $clause = (join $and, @all_sql); + @bind = $self->_bindtype($k, @all_bind); + } + my $sql = "( $label $op $clause )"; return ($sql, @bind) } @@ -752,47 +813,77 @@ sub _where_field_IN { sub _order_by { my ($self, $arg) = @_; - # construct list of ordering instructions - my @order = $self->_SWITCH_refkind($arg, { + my (@sql, @bind); + for my $c ($self->_order_by_chunks ($arg) ) { + $self->_SWITCH_refkind ($c, { + SCALAR => sub { push @sql, $c }, + ARRAYREF => sub { push @sql, shift @$c; push @bind, @$c }, + }); + } + + my $sql = @sql + ? sprintf ('%s %s', + $self->_sqlcase(' order by'), + join (', ', @sql) + ) + : '' + ; + + return wantarray ? ($sql, @bind) : $sql; +} + +sub _order_by_chunks { + my ($self, $arg) = @_; + + return $self->_SWITCH_refkind($arg, { ARRAYREF => sub { - map {$self->_SWITCH_refkind($_, { - SCALAR => sub {$self->_quote($_)}, - UNDEF => sub {}, - SCALARREF => sub {$$_}, # literal SQL, no quoting - HASHREF => sub {$self->_order_by_hash($_)} - }) } @$arg; + map { $self->_order_by_chunks ($_ ) } @$arg; }, + ARRAYREFREF => sub { [ @$$arg ] }, + SCALAR => sub {$self->_quote($arg)}, - UNDEF => sub {}, + + UNDEF => sub {return () }, + SCALARREF => sub {$$arg}, # literal SQL, no quoting - HASHREF => sub {$self->_order_by_hash($arg)}, - }); + HASHREF => sub { + # get first pair in hash + my ($key, $val) = each %$arg; - # build SQL - my $order = join ', ', @order; - return $order ? $self->_sqlcase(' order by')." $order" : ''; -} + return () unless $key; + if ( (keys %$arg) > 1 or not $key =~ /^-(desc|asc)/i ) { + puke "hash passed to _order_by must have exactly one key (-desc or -asc)"; + } -sub _order_by_hash { - my ($self, $hash) = @_; + my $direction = $1; - # get first pair in hash - my ($key, $val) = each %$hash; + my (@sql, @bind); + for my $c ($self->_order_by_chunks ($val)) { - # check if one pair was found and no other pair in hash - $key && !(each %$hash) - or puke "hash passed to _order_by must have exactly one key (-desc or -asc)"; - my ($order) = ($key =~ /^-(desc|asc)/i) - or puke "invalid key in _order_by hash : $key"; - return $self->_quote($val) ." ". $self->_sqlcase($order); -} + $self->_SWITCH_refkind ($c, { + SCALAR => sub { + push @sql, $c + }, + ARRAYREF => sub { + my ($s, @b) = @$c; + push @sql, $s; + push @bind, @b; + }, + }); + } + my $sql = join ', ', map { $_ . ' ' . $self->_sqlcase($direction) } @sql; + + return [$sql, @bind]; + }, + }); +} #====================================================================== @@ -1308,12 +1399,15 @@ the huge section on L at the bottom. =item sqltrue, sqlfalse Expressions for inserting boolean values within SQL statements. -By default these are C<1=1> and C<1=0>. +By default these are C<1=1> and C<1=0>. They are used +by the special operators C<-in> and C<-not_in> for generating +correct SQL even when the argument is an empty array (see below). =item logic This determines the default logical operator for multiple WHERE -statements in arrays. By default it is "or", meaning that a WHERE +statements in arrays or hashes. If absent, the default logic is "or" +for arrays, and "and" for hashes. This means that a WHERE array of the form: @where = ( @@ -1321,7 +1415,7 @@ array of the form: event_date => {'<=', '4/24/03'}, ); -Will generate SQL like this: +will generate SQL like this: WHERE event_date >= '2/13/99' OR event_date <= '4/24/03' @@ -1335,10 +1429,10 @@ Which will change the above C to: WHERE event_date >= '2/13/99' AND event_date <= '4/24/03' The logic can also be changed locally by inserting -an extra first element in the array : +a modifier in front of an arrayref : - @where = (-and => event_date => {'>=', '2/13/99'}, - event_date => {'<=', '4/24/03'} ); + @where = (-and => [event_date => {'>=', '2/13/99'}, + event_date => {'<=', '4/24/03'} ]); See the L section for explanations. @@ -1623,10 +1717,10 @@ This simple code will create the following: $stmt = "WHERE user = ? AND ( status = ? OR status = ? OR status = ? )"; @bind = ('nwiger', 'assigned', 'in-progress', 'pending'); -An empty arrayref will be considered a logical false and -will generate 0=1. +A field associated to an empty arrayref will be considered a +logical false and will generate 0=1. -=head2 Key-value pairs +=head2 Specific comparison operators If you want to specify a different type of operator for your comparison, you can use a hashref for a given column: @@ -1733,29 +1827,6 @@ Here is a quick list of equivalencies, since there is some overlap: status => [ -or => {'=', 'assigned'}, {'=', 'in-progress'}] status => [ {'=', 'assigned'}, {'=', 'in-progress'} ] -In addition to C<-and> and C<-or>, there is also a special C<-nest> -operator which adds an additional set of parens, to create a subquery. -For example, to get something like this: - - $stmt = "WHERE user = ? AND ( workhrs > ? OR geo = ? )"; - @bind = ('nwiger', '20', 'ASIA'); - -You would do: - - my %where = ( - user => 'nwiger', - -nest => [ workhrs => {'>', 20}, geo => 'ASIA' ], - ); - -If you need several nested subexpressions, you can number -the C<-nest> branches : - - my %where = ( - user => 'nwiger', - -nest1 => ..., - -nest2 => ..., - ... - ); =head2 Special operators : IN, BETWEEN, etc. @@ -1776,6 +1847,12 @@ Which would generate: The reverse operator C<-not_in> generates SQL C and is used in the same way. +If the argument to C<-in> is an empty array, 'sqlfalse' is generated +(by default : C<1=0>). Similarly, C<< -not_in => [] >> generates +'sqltrue' (by default : C<1=1>). + + + Another pair of operators is C<-between> and C<-not_between>, used with an arrayref of two values: @@ -1793,7 +1870,7 @@ Would give you: These are the two builtin "special operators"; but the list can be expanded : see section L below. -=head2 Nested conditions +=head2 Nested conditions, -and/-or prefixes So far, we've seen how multiple conditions are joined with a top-level C. We can change this by putting the different conditions we want in @@ -1816,15 +1893,32 @@ This data structure would create the following: OR ( user = ? AND status = ? ) )"; @bind = ('nwiger', 'pending', 'dispatched', 'robot', 'unassigned'); -This can be combined with the C<-nest> operator to properly group -SQL statements: + +There is also a special C<-nest> +operator which adds an additional set of parens, to create a subquery. +For example, to get something like this: + + $stmt = "WHERE user = ? AND ( workhrs > ? OR geo = ? )"; + @bind = ('nwiger', '20', 'ASIA'); + +You would do: + + my %where = ( + user => 'nwiger', + -nest => [ workhrs => {'>', 20}, geo => 'ASIA' ], + ); + + +Finally, clauses in hashrefs or arrayrefs can be +prefixed with an C<-and> or C<-or> to change the logic +inside : my @where = ( -and => [ user => 'nwiger', -nest => [ - ["-and", workhrs => {'>', 20}, geo => 'ASIA' ], - ["-and", workhrs => {'<', 50}, geo => 'EURO' ] + -and => [workhrs => {'>', 20}, geo => 'ASIA' ], + -and => [workhrs => {'<', 50}, geo => 'EURO' ] ], ], ); @@ -1835,6 +1929,37 @@ That would yield: ( ( workhrs > ? AND geo = ? ) OR ( workhrs < ? AND geo = ? ) ) ) + +=head2 Algebraic inconsistency, for historical reasons + +C: when connecting several conditions, the C<-and->|C<-or> +operator goes C of the nested structure; whereas when connecting +several constraints on one column, the C<-and> operator goes +C the arrayref. Here is an example combining both features : + + my @where = ( + -and => [a => 1, b => 2], + -or => [c => 3, d => 4], + e => [-and => {-like => 'foo%'}, {-like => '%bar'} ] + ) + +yielding + + WHERE ( ( ( a = ? AND b = ? ) + OR ( c = ? OR d = ? ) + OR ( e LIKE ? AND e LIKE ? ) ) ) + +This difference in syntax is unfortunate but must be preserved for +historical reasons. So be careful : the two examples below would +seem algebraically equivalent, but they are not + + {col => [-and => {-like => 'foo%'}, {-like => '%bar'}]} + # yields : WHERE ( ( col LIKE ? AND col LIKE ? ) ) + + [-and => {col => {-like => 'foo%'}, {col => {-like => '%bar'}}]] + # yields : WHERE ( ( col LIKE ? OR col LIKE ? ) ) + + =head2 Literal SQL Finally, sometimes only literal SQL will do. If you want to include @@ -2012,30 +2137,45 @@ Some functions take an order by clause. This can either be a scalar (just a column name,) a hash of C<< { -desc => 'col' } >> or C<< { -asc => 'col' } >>, or an array of either of the two previous forms. Examples: - Given | Will Generate + Given | Will Generate ---------------------------------------------------------- - \'colA DESC' | ORDER BY colA DESC - 'colA' | ORDER BY colA - [qw/colA colB/] | ORDER BY colA, colB - {-asc => 'colA'} | ORDER BY colA ASC - {-desc => 'colB'} | ORDER BY colB DESC - [ | - {-asc => 'colA'}, | ORDER BY colA ASC, colB DESC - {-desc => 'colB'} | - ] | - [colA => {-asc => 'colB'}] | ORDER BY colA, colB ASC - ========================================================== + | + \'colA DESC' | ORDER BY colA DESC + | + 'colA' | ORDER BY colA + | + [qw/colA colB/] | ORDER BY colA, colB + | + {-asc => 'colA'} | ORDER BY colA ASC + | + {-desc => 'colB'} | ORDER BY colB DESC + | + ['colA', {-asc => 'colB'}] | ORDER BY colA, colB ASC + | + { -asc => [qw/colA colB] } | ORDER BY colA ASC, colB ASC + | + [ | + { -asc => 'colA' }, | ORDER BY colA ASC, colB DESC, + { -desc => [qw/colB/], | colC ASC, colD ASC + { -asc => [qw/colC colD/],| + ] | + =========================================================== =head1 SPECIAL OPERATORS my $sqlmaker = SQL::Abstract->new(special_ops => [ - {regex => qr/.../, + { + regex => qr/.../, handler => sub { my ($self, $field, $op, $arg) = @_; ... - }, + }, + }, + { + regex => qr/.../, + handler => 'method_name', }, ]); @@ -2048,12 +2188,13 @@ For example : WHERE MATCH(field) AGAINST (?, ?) Special operators IN and BETWEEN are fairly standard and therefore -are builtin within C. For other operators, -like the MATCH .. AGAINST example above which is -specific to MySQL, you can write your own operator handlers : -supply a C argument to the C method. -That argument takes an arrayref of operator definitions; -each operator definition is a hashref with two entries +are builtin within C (as the overridable methods +C<_where_field_IN> and C<_where_field_BETWEEN>). For other operators, +like the MATCH .. AGAINST example above which is specific to MySQL, +you can write your own operator handlers - supply a C +argument to the C method. That argument takes an arrayref of +operator definitions; each operator definition is a hashref with two +entries: =over @@ -2063,10 +2204,24 @@ the regular expression to match the operator =item handler -coderef that will be called when meeting that operator -in the input tree. The coderef will be called with -arguments C<< ($self, $field, $op, $arg) >>, and -should return a C<< ($sql, @bind) >> structure. +Either a coderef or a plain scalar method name. In both cases +the expected return is C<< ($sql, @bind) >>. + +When supplied with a method name, it is simply called on the +L object as: + + $self->$method_name ($field, $op, $arg) + + Where: + + $op is the part that matched the handler regex + $field is the LHS of the operator + $arg is the RHS + +When supplied with a coderef, it is called as: + + $coderef->($self, $field, $op, $arg) + =back @@ -2178,10 +2333,6 @@ support for the { operator => \["...", @bind] } construct (to embed literal SQL =item * -added -nest1, -nest2 or -nest_1, -nest_2, ... - -=item * - optional support for L =item * @@ -2191,21 +2342,12 @@ defensive programming : check arguments =item * fixed bug with global logic, which was previously implemented -through global variables yielding side-effects. Prior versons would +through global variables yielding side-effects. Prior versions would interpret C<< [ {cond1, cond2}, [cond3, cond4] ] >> as C<< "(cond1 AND cond2) OR (cond3 AND cond4)" >>. Now this is interpreted as C<< "(cond1 AND cond2) OR (cond3 OR cond4)" >>. -=item * - -C<-and> / C<-or> operators are no longer accepted -in the middle of an arrayref : they are -only admitted if in first position. - -=item * - -changed logic for distributing an op over arrayrefs =item * @@ -2241,6 +2383,7 @@ so I have no idea who they are! But the people I do know are: Guillermo Roditi (patch to cleanup "IN" and "BETWEEN", fix and tests for _order_by) Laurent Dami (internal refactoring, multiple -nest, extensible list of special operators, literal SQL) Norbert Buchmuller (support for literal SQL in hashpair, misc. fixes & tests) + Peter Rabbitson (rewrite of SQLA::Test, misc. fixes & tests) Thanks! @@ -2259,6 +2402,8 @@ While not an official support venue, C makes heavy use of C, and as such list members there are very familiar with how to create queries. +=head1 LICENSE + This module is free software; you may copy this under the terms of the GNU General Public License, or the Artistic License, copies of which should have accompanied your Perl kit.