# GLOBALS
#======================================================================
-our $VERSION = '1.72';
+our $VERSION = '1.73';
# This would confuse some packagers
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
my @BUILTIN_SPECIAL_OPS = (
{regex => qr/^ (?: not \s )? between $/ix, handler => '_where_field_BETWEEN'},
{regex => qr/^ (?: not \s )? in $/ix, handler => '_where_field_IN'},
+ {regex => qr/^ ident $/ix, handler => '_where_op_IDENT'},
+ {regex => qr/^ value $/ix, handler => '_where_op_VALUE'},
);
# unaryish operators - key maps to handler
{ 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' },
+ { regex => qr/^ ident $/xi, handler => '_where_op_IDENT' },
+ { regex => qr/^ value $/ix, handler => '_where_op_VALUE' },
);
#======================================================================
}
+sub _where_op_IDENT {
+ my $self = shift;
+ my ($op, $rhs) = splice @_, -2;
+ if (ref $rhs) {
+ puke "-$op takes a single scalar argument (a quotable identifier)";
+ }
+
+ # in case we are called as a top level special op (no '=')
+ my $lhs = shift;
+
+ $_ = $self->_convert($self->_quote($_)) for ($lhs, $rhs);
+
+ return $lhs
+ ? "$lhs = $rhs"
+ : $rhs
+ ;
+}
+
+sub _where_op_VALUE {
+ my $self = shift;
+ my ($op, $rhs) = splice @_, -2;
+
+ # in case we are called as a top level special op (no '=')
+ my $lhs = shift;
+
+ my @bind =
+ $self->_bindtype (
+ ($lhs || $self->{_nested_func_lhs}),
+ $rhs,
+ )
+ ;
+
+ return $lhs
+ ? (
+ $self->_convert($self->_quote($lhs)) . ' = ' . $self->_convert('?'),
+ @bind
+ )
+ : (
+ $self->_convert('?'),
+ @bind,
+ )
+ ;
+}
+
sub _where_hashpair_ARRAYREF {
my ($self, $k, $v) = @_;
SCALARREF => sub {$$arg}, # literal SQL, no quoting
HASHREF => sub {
- # get first pair in hash
- my ($key, $val, @rest) = %$arg;
-
- return () unless $key;
-
- if ( @rest or not $key =~ /^-(desc|asc)/i ) {
- puke "hash passed to _order_by must have exactly one key (-desc or -asc)";
+ return () unless %$arg;
+
+ my ($direction, $nulls, $val);
+ foreach my $key (keys %$arg) {
+ if ( $key =~ /^-(desc|asc)/i ) {
+ puke "hash passed to _order_by must have exactly one of -desc or -asc"
+ if defined $direction;
+ $direction = $1;
+ $val = $arg->{$key};
+ } elsif ($key =~ /^-nulls$/i) {
+ $nulls = $arg->{$key};
+ puke "invalid value for -nulls" unless $nulls =~ /^(?:first|last)$/i;
+ } else {
+ puke "invalid key in hash passed to _order_by";
+ }
}
-
- my $direction = $1;
+ puke "hash passed to _order_by must have exactly one of -desc or -asc"
+ unless defined $direction;
my @ret;
for my $c ($self->_order_by_chunks ($val)) {
},
});
- $sql = $sql . ' ' . $self->_sqlcase($direction);
+ $sql .= ' ' . $self->_sqlcase($direction);
+ $sql .= ' ' . $self->_sqlcase("nulls $nulls")
+ if defined $nulls;
push @ret, [ $sql, @bind];
}
my $sql = SQL::Abstract->new;
- my($stmt, @bind) = $sql->select($table, \@fields, \%where, \@order);
+ my($stmt, @bind) = $sql->select($source, \@fields, \%where, \@order);
my($stmt, @bind) = $sql->insert($table, \%fieldvals || \@values);
The argument can be either an arrayref (interpreted as a list
of field names, will be joined by commas and quoted), or a
plain scalar (literal SQL, not quoted).
-Please observe that this API is not as flexible as for
-the first argument C<$table>, for backwards compatibility reasons.
+Please observe that this API is not as flexible as that of
+the first argument C<$source>, for backwards compatibility reasons.
=item $where
You get the idea. Strings get their case twiddled, but everything
else remains verbatim.
-
-
-
=head1 WHERE CLAUSES
=head2 Introduction
user => 'nwiger',
status => { '!=', undef },
);
-
+
=head2 Specific comparison operators
If you want to specify a different type of operator for your comparison,
OR ( workhrs < ? OR geo = ? )
) )
-=head2 Algebraic inconsistency, for historical reasons
+=head3 Algebraic inconsistency, for historical reasons
C<Important note>: when connecting several conditions, the C<-and->|C<-or>
operator goes C<outside> of the nested structure; whereas when connecting
# yields : WHERE ( ( col LIKE ? OR col LIKE ? ) )
-=head2 Literal SQL
+=head2 Literal SQL and value type operators
+
+The basic premise of SQL::Abstract is that in WHERE specifications the "left
+side" is a column name and the "right side" is a value (normally rendered as
+a placeholder). This holds true for both hashrefs and arrayref pairs as you
+see in the L</WHERE CLAUSES> examples above. Sometimes it is necessary to
+alter this behavior. There are several ways of doing so.
-Finally, sometimes only literal SQL will do.
-To include literal SQL verbatim, you specify it as a scalar reference.
-Consider this only as a last resort. Usually there is a better way.
+=head3 -ident
-Literal SQL is the only way to compare 2 columns to one another:
+This is a virtual operator that signals the string to its right side is an
+identifier (a column name) and not a value. For example to compare two
+columns you would write:
my %where = (
priority => { '<', 2 },
- requestor => \'= submittor'
+ requestor => { -ident => 'submitter' },
);
which creates:
$stmt = "WHERE priority < ? AND requestor = submitter";
@bind = ('2');
+If you are maintaining legacy code you may see a different construct as
+described in L</Deprecated usage of Literal SQL>, please use C<-ident> in new
+code.
+
+=head3 -value
-There is a nicer way to test for NULL, but just for the sake of example:
+This is a virtual operator that signals that the construct to its right side
+is a value to be passed to DBI. This is for example necessary when you want
+to write a where clause against an array (for RDBMS that support such
+datatypes). For example:
- my $inn = 'IS NOT NULL';
my %where = (
- priority => { '<', 2 },
- requestor => \$inn
+ array => { -value => [1, 2, 3] }
);
-This would create:
+will result in:
- $stmt = "WHERE priority < ? AND requestor is Not Null";
- @bind = ('2');
-
-Note that in this example, you only get one bind parameter back, since
-the verbatim SQL is passed as part of the statement.
+ $stmt = 'WHERE array = ?';
+ @bind = ([1, 2, 3]);
-Of course, just to prove a point, the above can also be accomplished
-with this:
+Note that if you were to simply say:
my %where = (
- priority => { '<', 2 },
- requestor => { '!=', undef },
+ array => [1, 2, 3]
);
+the result would porbably be not what you wanted:
-Conditions on boolean columns can be expressed by 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</"Unary operators: bool"> :
+ $stmt = 'WHERE array = ? OR array = ? OR array = ?';
+ @bind = (1, 2, 3);
+
+=head3 Literal SQL
+
+Finally, sometimes only literal SQL will do. To include a random snippet
+of SQL verbatim, you specify it as a scalar reference. Consider this only
+as a last resort. Usually there is a better way. For example:
my %where = (
- priority => { '<', 2 },
- is_ready => \"";
+ priority => { '<', 2 },
+ requestor => { -in => \'(SELECT name FROM hitmen)' },
);
-which yields
+Would create:
- $stmt = "WHERE priority < ? AND is_ready";
- @bind = ('2');
+ $stmt = "WHERE priority < ? AND requestor IN (SELECT name FROM hitmen)"
+ @bind = (2);
+
+Note that in this example, you only get one bind parameter back, since
+the verbatim SQL is passed as part of the statement.
+
+=head4 CAVEAT
-=head2 Literal SQL with placeholders and bind values (subqueries)
+ Never use untrusted input as a literal SQL argument - this is a massive
+ security risk (there is no way to check literal snippets for SQL
+ injections and other nastyness). If you need to deal with untrusted input
+ use literal SQL with placeholders as described next.
+
+=head3 Literal SQL with placeholders and bind values (subqueries)
If the literal SQL to be inserted has placeholders and bind values,
use a reference to an arrayref (yes this is a double reference --
$stmt = "lname LIKE ? AND NOT ( age < ? OR age > ? )"
@bind = ('%son%', 10, 20)
+=head3 Deprecated usage of Literal SQL
+
+Below are some examples of archaic use of literal SQL. It is shown only as
+reference for those who deal with legacy code. Each example has a much
+better, cleaner and safer alternative that users should opt for in new code.
+
+=over
+
+=item *
+
+ my %where = ( requestor => \'IS NOT NULL' )
+
+ $stmt = "WHERE requestor IS NOT NULL"
+
+This used to be the way of generating NULL comparisons, before the handling
+of C<undef> got formalized. For new code please use the superior syntax as
+described in L</Tests for NULL values>.
+=item *
+
+ my %where = ( requestor => \'= submitter' )
+
+ $stmt = "WHERE requestor = submitter"
+
+This used to be the only way to compare columns. Use the superior L</-ident>
+method for all new code. For example an identifier declared in such a way
+will be properly quoted if L</quote_char> is properly set, while the legacy
+form will remain as supplied.
+
+=item *
+
+ my %where = ( is_ready => \"", completed => { '>', '2012-12-21' } )
+
+ $stmt = "WHERE completed > ? AND is_ready"
+ @bind = ('2012-12-21')
+
+Using an empty string literal used to be the only way to express a boolean.
+For all new code please use the much more readable
+L<-bool|/Unary operators: bool> operator.
+
+=back
=head2 Conclusion
dynamically-generating SQL and could just hardwire it into your
script.
-
-
-
=head1 ORDER BY CLAUSES
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' } >>,
+optionally with C<< -nulls => 'first' >> or C<< -nulls => 'last' >>,
or an array of either of the two previous forms. Examples:
Given | Will Generate
|
{-desc => 'colB'} | ORDER BY colB DESC
|
+ { |
+ -asc => 'colA', | ORDER BY colA ASC NULLS LAST
+ -nulls => 'last', |
+ } |
+ |
['colA', {-asc => 'colB'}] | ORDER BY colA, colB ASC
|
{ -asc => [qw/colA colB/] } | ORDER BY colA ASC, colB ASC
|
+ { |
+ -asc => [qw/colA colB/] | ORDER BY colA ASC NULLS FIRST,
+ -nulls => 'first' | colB ASC NULLS FIRST
+ } |
+ |
[ |
{ -asc => 'colA' }, | ORDER BY colA ASC, colB DESC,
{ -desc => [qw/colB/], | colC ASC, colD ASC
=back
-
-
=head1 ACKNOWLEDGEMENTS
There are a number of individuals that have really helped out with