X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FSQL%2FAbstract.pm;h=3888f657d1dba8161a61b596f19b6b5855c034e4;hb=91778193f84bc4aa93058ac326fcd122011015cd;hp=5cae877839db346310f9ff0fa06af41024faa2f9;hpb=8c737502095a70e8e48c61ac4d88020a7673e550;p=scpubgit%2FQ-Branch.git diff --git a/lib/SQL/Abstract.pm b/lib/SQL/Abstract.pm index 5cae877..3888f65 100644 --- a/lib/SQL/Abstract.pm +++ b/lib/SQL/Abstract.pm @@ -15,7 +15,7 @@ use Scalar::Util qw/blessed/; # GLOBALS #====================================================================== -our $VERSION = '1.56'; +our $VERSION = '1.61'; # This would confuse some packagers #$VERSION = eval $VERSION; # numify for warning-free dev releases @@ -31,10 +31,11 @@ my @BUILTIN_SPECIAL_OPS = ( # unaryish operators - key maps to handler my @BUILTIN_UNARY_OPS = ( - { 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' }, + # 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' }, ); #====================================================================== @@ -81,10 +82,18 @@ sub new { # default comparison is "=", but can be overridden $opt{cmp} ||= '='; + # generic SQL comparison operators + my $anchored_cmp_ops = join ('|', map { '^' . $_ . '$' } ( + '(?:is \s+)? (?:not \s+)? like', + 'is', + (map { quotemeta($_) } (qw/ < > != <> = <= >= /) ), + )); + $opt{cmp_ops} = qr/$anchored_cmp_ops/ix; + # try to recognize which are the 'equality' and 'unequality' ops # (temporary quickfix, should go through a more seasoned API) - $opt{equality_op} = qr/^(\Q$opt{cmp}\E|is|(is\s+)?like)$/i; - $opt{inequality_op} = qr/^(!=|<>|(is\s+)?not(\s+like)?)$/i; + $opt{equality_op} = qr/^(\Q$opt{cmp}\E|is|(is\s+)?like)$/i; + $opt{inequality_op} = qr/^(!=|<>|(is\s+)?not(\s+like)?)$/i; # SQL booleans $opt{sqltrue} ||= '1=1'; @@ -108,16 +117,33 @@ sub new { #====================================================================== sub insert { - my $self = shift; - my $table = $self->_table(shift); - my $data = shift || return; + my $self = shift; + my $table = $self->_table(shift); + my $data = shift || return; + my $options = shift; my $method = $self->_METHOD_FOR_refkind("_insert", $data); - my ($sql, @bind) = $self->$method($data); + my ($sql, @bind) = $self->$method($data); $sql = join " ", $self->_sqlcase('insert into'), $table, $sql; + + if (my $ret = $options->{returning}) { + $sql .= $self->_insert_returning ($ret); + } + return wantarray ? ($sql, @bind) : $sql; } +sub _insert_returning { + my ($self, $fields) = @_; + + my $f = $self->_SWITCH_refkind($fields, { + ARRAYREF => sub {join ', ', map { $self->_quote($_) } @$fields;}, + SCALAR => sub {$self->_quote($fields)}, + SCALARREF => sub {$$fields}, + }); + return join (' ', $self->_sqlcase(' returning'), $f); +} + sub _insert_HASHREF { # explicit list of fields and then values my ($self, $data) = @_; @@ -434,15 +460,46 @@ sub _where_HASHREF { my ($self, $where) = @_; my (@sql_clauses, @all_bind); - for my $k (sort keys %$where) { + for my $k (sort keys %$where) { 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) - : do { - my $method = $self->_METHOD_FOR_refkind("_where_hashpair", $v); - $self->$method($k, $v); - }; + # ($k => $v) is either a special unary op or a regular hashpair + my ($sql, @bind) = do { + if ($k =~ /^-./) { + # put the operator in canonical form + my $op = $k; + $op =~ s/^-//; # remove initial dash + $op =~ s/[_\t ]+/ /g; # underscores and whitespace become single spaces + $op =~ s/^\s+|\s+$//g;# remove leading/trailing space + + $self->_debug("Unary OP(-$op) within hashref, recursing..."); + + my $op_entry = first {$op =~ $_->{regex}} @{$self->{unary_ops}}; + if (my $handler = $op_entry->{handler}) { + if (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 ... ]"; + } + $self->$handler ($op, $v); + } + elsif (ref $handler eq 'CODE') { + $handler->($self, $op, $v); + } + else { + puke "Illegal handler for operator $k - expecting a method name or a coderef"; + } + } + else { + $self->debug("Generic unary OP: $k - recursing as function"); + $self->_where_func_generic ($op, $v); + } + } + else { + my $method = $self->_METHOD_FOR_refkind("_where_hashpair", $v); + $self->$method($k, $v); + } + }; push @sql_clauses, $sql; push @all_bind, @bind; @@ -451,42 +508,34 @@ sub _where_HASHREF { return $self->_join_sql_clauses('and', \@sql_clauses, \@all_bind); } +sub _where_func_generic { + my ($self, $op, $rhs) = @_; -sub _where_op_in_hash { - my ($self, $op, $v) = @_; + my ($sql, @bind) = $self->_SWITCH_refkind ($rhs, { + SCALAR => sub { + puke "Illegal use of top-level '$op'" + unless $self->{_nested_func_lhs}; - # 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 + return ( + $self->_convert('?'), + $self->_bindtype($self->{_nested_func_lhs}, $rhs) + ); + }, + FALLBACK => sub { + $self->_recurse_where ($rhs) + }, + }); - $self->_debug("OP(-$op) within hashref, recursing..."); + $sql = sprintf ('%s%s', + $self->_sqlcase($op), + ($op =~ $self->{cmp_ops}) ? " $sql" : "( $sql )", + ); - my $op_entry = first {$op =~ $_->{regex}} @{$self->{unary_ops}}; - my $handler = $op_entry->{handler}; - if (! $handler) { - puke "unknown operator: -$op"; - } - elsif (not ref $handler) { - return $self->$handler ($op, $v); - } - elsif (ref $handler eq 'CODE') { - return $handler->($self, $op, $v); - } - else { - puke "Illegal handler for operator $op - expecting a method name or a coderef"; - } + return ($sql, @bind); } sub _where_op_ANDOR { - my ($self, $op, $v) = @_; - - 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 ... ]"; - } + my ($self, $op, $v) = @_; $self->_SWITCH_refkind($v, { ARRAYREF => sub { @@ -520,30 +569,8 @@ sub _where_op_ANDOR { sub _where_op_NEST { my ($self, $op, $v) = @_; - 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 ... ]"; - } - - $self->_SWITCH_refkind($v, { - ARRAYREF => sub { - return $self->_where_ARRAYREF($v, ''); - }, - - HASHREF => sub { - return $self->_where_HASHREF($v); - }, - - SCALARREF => sub { # literal SQL - return ($$v); - }, - - ARRAYREFREF => sub { # literal SQL - return @{${$v}}; - }, - SCALAR => sub { # permissively interpreted as SQL belch "literal SQL should be -nest => \\'scalar' " . "instead of -nest => 'scalar' "; @@ -553,6 +580,11 @@ sub _where_op_NEST { UNDEF => sub { puke "-$op => undef not supported"; }, + + FALLBACK => sub { + $self->_recurse_where ($v); + }, + }); } @@ -560,16 +592,30 @@ sub _where_op_NEST { sub _where_op_BOOL { my ($self, $op, $v) = @_; - my $prefix = ($op =~ /\bnot\b/i) ? 'NOT ' : ''; - $self->_SWITCH_refkind($v, { - SCALARREF => sub { # literal SQL - return ($prefix . $$v); - }, + my ( $prefix, $suffix ) = ( $op =~ /\bnot\b/i ) + ? ( '(NOT ', ')' ) + : ( '', '' ); - SCALAR => sub { # interpreted as SQL column - return ($prefix . $self->_convert($self->_quote($v))); - }, - }); + my ($sql, @bind) = do { + $self->_SWITCH_refkind($v, { + SCALAR => sub { # interpreted as SQL column + $self->_convert($self->_quote($v)); + }, + + UNDEF => sub { + puke "-$op => undef not supported"; + }, + + FALLBACK => sub { + $self->_recurse_where ($v); + }, + }); + }; + + return ( + join ('', $prefix, $sql, $suffix), + @bind, + ); } @@ -608,26 +654,31 @@ sub _where_hashpair_HASHREF { my ($self, $k, $v, $logic) = @_; $logic ||= 'and'; + local $self->{_nested_func_lhs} = $self->{_nested_func_lhs}; + $self->{_nested_func_lhs} ||= $k; + 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: col-value logic modifiers + if ( $orig_op =~ /^ \- (and|or) $/xi ) { + ($sql, @bind) = $self->_where_hashpair_HASHREF($k, $val, $1); + } # CASE: special operators like -in or -between - my $special_op = first {$op =~ $_->{regex}} @{$self->{special_ops}}; - if ($special_op) { + elsif ( my $special_op = first {$op =~ $_->{regex}} @{$self->{special_ops}} ) { my $handler = $special_op->{handler}; if (! $handler) { - puke "No handler supplied for special operator matching $special_op->{regex}"; + puke "No handler supplied for special operator $orig_op"; } elsif (not ref $handler) { ($sql, @bind) = $self->$handler ($k, $op, $val); @@ -636,7 +687,7 @@ sub _where_hashpair_HASHREF { ($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"; + puke "Illegal handler for special operator $orig_op - expecting a method name or a coderef"; } } else { @@ -646,12 +697,6 @@ sub _where_hashpair_HASHREF { ($sql, @bind) = $self->_where_field_op_ARRAYREF($k, $op, $val); }, - SCALARREF => sub { # CASE: col => {op => \$scalar} (literal SQL without bind) - $sql = join ' ', $self->_convert($self->_quote($k)), - $self->_sqlcase($op), - $$val; - }, - ARRAYREFREF => sub { # CASE: col => {op => \[$sql, @bind]} (literal SQL with bind) my ($sub_sql, @sub_bind) = @$$val; $self->_assert_bindval_matches_bindtype(@sub_bind); @@ -661,22 +706,16 @@ 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' : - 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), - $self->_convert('?'); - @bind = $self->_bindtype($k, $val); + + FALLBACK => sub { # CASE: col => {op/func => $stuff} + ($sql, @bind) = $self->_where_func_generic ($op, $val); + $sql = join ' ', $self->_convert($self->_quote($k)), $sql; }, }); } @@ -695,11 +734,14 @@ sub _where_field_op_ARRAYREF { my @vals = @$vals; #always work on a copy if(@vals) { - $self->_debug("ARRAY($vals) means multiple elements: [ @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; } @@ -802,38 +844,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) } @@ -864,21 +919,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* $/xs; + return $sql; +} #====================================================================== @@ -1634,7 +1701,7 @@ See section L for details. =back -=head2 insert($table, \@values || \%fieldvals) +=head2 insert($table, \@values || \%fieldvals, \%options) This is the simplest function. You simply give it a table name and either an arrayref of values or hashref of field/value pairs. @@ -1643,6 +1710,23 @@ See the sections on L and L for information on how to insert with those data types. +The optional C<\%options> hash reference may contain additional +options to generate the insert SQL. Currently supported options +are: + +=over 4 + +=item returning + +Takes either a scalar of raw SQL fields, or an array reference of +field names, and adds on an SQL C statement at the end. +This allows you to return data generated by the insert statement +(such as row IDs) without performing another C