X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FSQL%2FAbstract.pm;h=e8bb10072ab3565086661993881746ac199033ee;hb=0f2b8636560c4634ec2c73116b0a3dbef2052405;hp=127e6ad30558bb7724ed4f96a6c8a7149be290f7;hpb=01a01e578da5a9743771d793d81dcbbb7f965183;p=scpubgit%2FQ-Branch.git diff --git a/lib/SQL/Abstract.pm b/lib/SQL/Abstract.pm index 127e6ad..e8bb100 100644 --- a/lib/SQL/Abstract.pm +++ b/lib/SQL/Abstract.pm @@ -15,7 +15,7 @@ use Scalar::Util qw/blessed/; # GLOBALS #====================================================================== -our $VERSION = '1.53'; +our $VERSION = '1.60'; # This would confuse some packagers #$VERSION = eval $VERSION; # numify for warning-free dev releases @@ -25,8 +25,17 @@ 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'}, +); + +# unaryish operators - key maps to handler +my @BUILTIN_UNARY_OPS = ( + # the digits are backcompat stuff + { regex => qr/^and (?: \s? \d+ )? $/xi, handler => '_where_op_ANDOR' }, + { regex => qr/^or (?: \s? \d+ )? $/xi, handler => '_where_op_ANDOR' }, + { regex => qr/^nest (?: \s? \d+ )? $/xi, handler => '_where_op_NEST' }, + { regex => qr/^ (?: not \s )? bool $/xi, handler => '_where_op_BOOL' }, ); #====================================================================== @@ -86,6 +95,10 @@ sub new { $opt{special_ops} ||= []; push @{$opt{special_ops}}, @BUILTIN_SPECIAL_OPS; + # unary operators + $opt{unary_ops} ||= []; + push @{$opt{unary_ops}}, @BUILTIN_UNARY_OPS; + return bless \%opt, $class; } @@ -426,7 +439,7 @@ sub _where_HASHREF { my $v = $where->{$k}; # ($k => $v) is either a special op or a regular hashpair - my ($sql, @bind) = ($k =~ /^-(.+)/) ? $self->_where_op_in_hash($1, $v) + my ($sql, @bind) = ($k =~ /^(-.+)/) ? $self->_where_op_in_hash($1, $v) : do { my $method = $self->_METHOD_FOR_refkind("_where_hashpair", $v); $self->$method($k, $v); @@ -441,49 +454,90 @@ sub _where_HASHREF { sub _where_op_in_hash { - my ($self, $op_str, $v) = @_; + my ($self, $orig_op, $v) = @_; - $op_str =~ /^ (AND|OR|NEST) ( \_? \d* ) $/xi - or puke "unknown operator: -$op_str"; + # put the operator in canonical form + my $op = $orig_op; + $op =~ s/^-//; # remove initial dash + $op =~ s/[_\t ]+/ /g; # underscores and whitespace become single spaces + $op =~ s/^\s+|\s+$//g;# remove leading/trailing space - 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 ... ]"; + $self->_debug("OP(-$op) within hashref, recursing..."); + + my $op_entry = first {$op =~ $_->{regex}} @{$self->{unary_ops}}; + my $handler = $op_entry->{handler}; + if (! $handler) { + puke "unknown operator: $orig_op"; + } + elsif (not ref $handler) { + if ($op =~ s/\s?\d+$//) { + belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. ' + . "You probably wanted ...-and => [ -$op => COND1, -$op => COND2 ... ]"; + } + return $self->$handler ($op, $v); + } + elsif (ref $handler eq 'CODE') { + return $handler->($self, $op, $v); + } + else { + puke "Illegal handler for operator $orig_op - expecting a method name or a coderef"; } +} - $self->_debug("OP(-$op) within hashref, recursing..."); +sub _where_op_ANDOR { + my ($self, $op, $v) = @_; + + $self->_SWITCH_refkind($v, { + ARRAYREF => sub { + return $self->_where_ARRAYREF($v, $op); + }, + + HASHREF => sub { + return ( $op =~ /^or/i ) + ? $self->_where_ARRAYREF( [ map { $_ => $v->{$_} } ( sort keys %$v ) ], $op ) + : $self->_where_HASHREF($v); + }, + + SCALARREF => sub { + puke "-$op => \\\$scalar not supported, use -nest => ..."; + }, + + ARRAYREFREF => sub { + puke "-$op => \\[..] not supported, use -nest => ..."; + }, + + SCALAR => sub { # permissively interpreted as SQL + puke "-$op => 'scalar' not supported, use -nest => \\'scalar'"; + }, + + UNDEF => sub { + puke "-$op => undef not supported"; + }, + }); +} + +sub _where_op_NEST { + my ($self, $op, $v) = @_; $self->_SWITCH_refkind($v, { ARRAYREF => sub { - return $self->_where_ARRAYREF($v, $op eq 'NEST' ? '' : $op); + return $self->_where_ARRAYREF($v, ''); }, HASHREF => sub { - if ($op eq 'OR') { - return $self->_where_ARRAYREF([ map { $_ => $v->{$_} } (sort keys %$v) ], 'OR'); - } - else { # NEST | AND - return $self->_where_HASHREF($v); - } + return $self->_where_HASHREF($v); }, SCALARREF => sub { # literal SQL - $op eq 'NEST' - or puke "-$op => \\\$scalar not supported, use -nest => ..."; return ($$v); }, ARRAYREFREF => sub { # literal SQL - $op eq 'NEST' - or puke "-$op => \\[..] not supported, use -nest => ..."; return @{${$v}}; }, SCALAR => sub { # permissively interpreted as SQL - $op eq 'NEST' - or puke "-$op => 'scalar' not supported, use -nest => \\'scalar'"; belch "literal SQL should be -nest => \\'scalar' " . "instead of -nest => 'scalar' "; return ($v); @@ -496,6 +550,43 @@ sub _where_op_in_hash { } +sub _where_op_BOOL { + my ($self, $op, $v) = @_; + + my ( $prefix, $suffix ) = ( $op =~ /\bnot\b/i ) + ? ( '(NOT ', ')' ) + : ( '', '' ); + $self->_SWITCH_refkind($v, { + ARRAYREF => sub { + my ( $sql, @bind ) = $self->_where_ARRAYREF($v, ''); + return ( ($prefix . $sql . $suffix), @bind ); + }, + + ARRAYREFREF => sub { + my ( $sql, @bind ) = @{ ${$v} }; + return ( ($prefix . $sql . $suffix), @bind ); + }, + + HASHREF => sub { + my ( $sql, @bind ) = $self->_where_HASHREF($v); + return ( ($prefix . $sql . $suffix), @bind ); + }, + + SCALARREF => sub { # literal SQL + return ($prefix . $$v . $suffix); + }, + + SCALAR => sub { # interpreted as SQL column + return ($prefix . $self->_convert($self->_quote($v)) . $suffix); + }, + + UNDEF => sub { + puke "-$op => undef not supported"; + }, + }); +} + + sub _where_hashpair_ARRAYREF { my ($self, $k, $v) = @_; @@ -533,22 +624,33 @@ sub _where_hashpair_HASHREF { my ($all_sql, @all_bind); - for my $op (sort keys %$v) { - my $val = $v->{$op}; + for my $orig_op (sort keys %$v) { + my $val = $v->{$orig_op}; # put the operator in canonical form - $op =~ s/^-//; # remove initial dash - $op =~ tr/_/ /; # underscores become spaces - $op =~ s/^\s+//; # no initial space - $op =~ s/\s+$//; # no final space - $op =~ s/\s+/ /; # multiple spaces become one + my $op = $orig_op; + $op =~ s/^-//; # remove initial dash + $op =~ s/[_\t ]+/ /g; # underscores and whitespace become single spaces + $op =~ s/^\s+|\s+$//g;# remove leading/trailing space my ($sql, @bind); # 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 $orig_op"; + } + 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 $orig_op - expecting a method name or a coderef"; + } } else { $self->_SWITCH_refkind($val, { @@ -579,10 +681,10 @@ sub _where_hashpair_HASHREF { UNDEF => sub { # CASE: col => {op => undef} : sql "IS (NOT)? NULL" my $is = ($op =~ $self->{equality_op}) ? 'is' : ($op =~ $self->{inequality_op}) ? 'is not' : - puke "unexpected operator '$op' with undef operand"; + puke "unexpected operator '$orig_op' with undef operand"; $sql = $self->_quote($k) . $self->_sqlcase(" $is null"); }, - + FALLBACK => sub { # CASE: col => {op => $scalar} $sql = join ' ', $self->_convert($self->_quote($k)), $self->_sqlcase($op), @@ -603,18 +705,23 @@ 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(sprintf '%s means multiple elements: [ %s ]', + $vals, + join (', ', map { defined $_ ? "'$_'" : 'NULL' } @vals ), + ); # see if the first element is an -and/-or op my $logic; - if ($vals->[0] =~ /^ - ( AND|OR ) $/ix) { + if (defined $vals[0] && $vals[0] =~ /^ - ( AND|OR ) $/ix) { $logic = uc $1; - shift @$vals; + shift @vals; } - # distribute $op over each remaining member of @$vals, append logic if exists - return $self->_recurse_where([map { {$k => {$op, $_}} } @$vals], $logic); + # 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 : @@ -623,7 +730,7 @@ sub _where_field_op_ARRAYREF { # WHERE field != 22 AND field != 33. # 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); } else { @@ -711,38 +818,51 @@ sub _where_UNDEF { sub _where_field_BETWEEN { my ($self, $k, $op, $vals) = @_; - (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 ($clause, @bind, $label, $and, $placeholder); + my ($label, $and, $placeholder); $label = $self->_convert($self->_quote($k)); $and = ' ' . $self->_sqlcase('and') . ' '; $placeholder = $self->_convert('?'); $op = $self->_sqlcase($op); - 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; - } + my ($clause, @bind) = $self->_SWITCH_refkind($vals, { + ARRAYREFREF => sub { + return @$$vals; + }, + SCALARREF => sub { + return $$vals; + }, + ARRAYREF => sub { + puke "special op 'between' accepts an arrayref with exactly two values" + if @$vals != 2; + + 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), ()); + }, + ARRAYREFREF => sub { + my ($sql, @bind) = @$$val; + return ($self->_convert($sql), @bind); + }, + }); + push @all_sql, $sql; + push @all_bind, @bind; + } + + return ( + (join $and, @all_sql), + $self->_bindtype($k, @all_bind), + ); + }, + FALLBACK => sub { + puke "special op 'between' accepts an arrayref with two values, or a single literal scalarref/arrayref-ref"; + }, + }); - $clause = (join $and, @all_sql); - @bind = $self->_bindtype($k, @all_bind); - } my $sql = "( $label $op $clause )"; return ($sql, @bind) } @@ -773,23 +893,33 @@ sub _where_field_IN { } }, + SCALARREF => sub { # literal SQL + my $sql = $self->_open_outer_paren ($$vals); + return ("$label $op ( $sql )"); + }, ARRAYREFREF => sub { # literal SQL with bind my ($sql, @bind) = @$$vals; $self->_assert_bindval_matches_bindtype(@bind); + $sql = $self->_open_outer_paren ($sql); return ("$label $op ( $sql )", @bind); }, FALLBACK => sub { - puke "special op 'in' requires an arrayref (or arrayref-ref)"; + puke "special op 'in' requires an arrayref (or scalarref/arrayref-ref)"; }, }); return ($sql, @bind); } - - - +# Some databases (SQLite) treat col IN (1, 2) different from +# col IN ( (1, 2) ). Use this to strip all outer parens while +# adding them back in the corresponding method +sub _open_outer_paren { + my ($self, $sql) = @_; + $sql = $1 while $sql =~ /^ \s* \( (.*) \) \s* $/x; + return $sql; +} #====================================================================== @@ -799,48 +929,76 @@ 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 @ret; + for my $c ($self->_order_by_chunks ($val)) { + my ($sql, @bind); - # 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)"; + $self->_SWITCH_refkind ($c, { + SCALAR => sub { + $sql = $c; + }, + ARRAYREF => sub { + ($sql, @bind) = @$c; + }, + }); - my ($order) = ($key =~ /^-(desc|asc)/i) - or puke "invalid key in _order_by hash : $key"; + $sql = $sql . ' ' . $self->_sqlcase($direction); - $val = ref $val eq 'ARRAY' ? $val : [$val]; - return join ', ', map { $self->_quote($_) . ' ' . $self->_sqlcase($order) } @$val; -} + push @ret, [ $sql, @bind]; + } + return @ret; + }, + }); +} #====================================================================== @@ -1507,6 +1665,12 @@ Takes a reference to a list of "special operators" to extend the syntax understood by L. See section L for details. +=item unary_ops + +Takes a reference to a list of "unary operators" +to extend the syntax understood by L. +See section L for details. + =back @@ -1808,9 +1972,28 @@ 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>). +In addition to the array you can supply a chunk of literal sql or +literal sql with bind: + my %where = { + customer => { -in => \[ + 'SELECT cust_id FROM cust WHERE balance > ?', + 2000, + ], + status => { -in => \'SELECT status_codes FROM states' }, + }; -Another pair of operators is C<-between> and C<-not_between>, +would generate: + + $stmt = "WHERE ( + customer IN ( SELECT cust_id FROM cust WHERE balance > ? ) + AND status IN ( SELECT status_codes FROM states ) + )"; + @bind = ('2000'); + + + +Another pair of operators is C<-between> and C<-not_between>, used with an arrayref of two values: my %where = ( @@ -1824,9 +2007,66 @@ Would give you: WHERE user = ? AND completion_date NOT BETWEEN ( ? AND ? ) +Just like with C<-in> all plausible combinations of literal SQL +are possible: + + my %where = { + start0 => { -between => [ 1, 2 ] }, + start1 => { -between => \["? AND ?", 1, 2] }, + start2 => { -between => \"lower(x) AND upper(y)" }, + start3 => { -between => [ + \"lower(x)", + \["upper(?)", 'stuff' ], + ] }, + }; + +Would give you: + + $stmt = "WHERE ( + ( start0 BETWEEN ? AND ? ) + AND ( start1 BETWEEN ? AND ? ) + AND ( start2 BETWEEN lower(x) AND upper(y) ) + AND ( start3 BETWEEN lower(x) AND upper(?) ) + )"; + @bind = (1, 2, 1, 2, 'stuff'); + + These are the two builtin "special operators"; but the list can be expanded : see section L below. +=head2 Unary operators: bool + +If you wish to test against boolean columns or functions within your +database you can use the C<-bool> and C<-not_bool> operators. For +example to test the column C being true and the column + being false you would use:- + + my %where = ( + -bool => 'is_user', + -not_bool => 'is_enabled', + ); + +Would give you: + + WHERE is_user AND NOT is_enabled + +If a more complex combination is required, testing more conditions, +then you should use the and/or operators:- + + my %where = ( + -and => [ + -bool => 'one', + -bool => 'two', + -bool => 'three', + -not_bool => 'four', + ], + ); + +Would give you: + + WHERE one AND two AND three AND NOT four + + =head2 Nested conditions, -and/-or prefixes So far, we've seen how multiple conditions are joined with a top-level @@ -1945,10 +2185,12 @@ with this: ); -TMTOWTDI. +TMTOWTDI -Conditions on boolean columns can be expressed in the -same way, passing a reference to an empty string : +Conditions on boolean columns can be expressed in the same way, passing +a reference to an empty string, however using liternal SQL in this way +is deprecated - the preferred method is to use the boolean operators - +see L : my %where = ( priority => { '<', 2 }, @@ -2123,11 +2365,16 @@ or an array of either of the two previous forms. Examples: =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', }, ]); @@ -2140,12 +2387,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 @@ -2155,10 +2403,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 @@ -2185,6 +2447,59 @@ of the MATCH .. AGAINST syntax for MySQL ]); +=head1 UNARY OPERATORS + + my $sqlmaker = SQL::Abstract->new(unary_ops => [ + { + regex => qr/.../, + handler => sub { + my ($self, $op, $arg) = @_; + ... + }, + }, + { + regex => qr/.../, + handler => 'method_name', + }, + ]); + +A "unary operator" is a SQL syntactic clause that can be +applied to a field - the operator goes before the field + +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 + +=item regex + +the regular expression to match the operator + +=item handler + +Either a coderef or a plain scalar method name. In both cases +the expected return is C<< $sql >>. + +When supplied with a method name, it is simply called on the +L object as: + + $self->$method_name ($op, $arg) + + Where: + + $op is the part that matched the handler regex + $arg is the RHS or argument of the operator + +When supplied with a coderef, it is called as: + + $coderef->($self, $op, $arg) + + +=back + + =head1 PERFORMANCE Thanks to some benchmarking by Mark Stosberg, it turns out that