@@ -1,5 +1,14 @@
Revision history for SQL::Abstract
+revision 1.79 2014-09-25
+----------------------------
+ - New exportable functions: is_literal_value($) and is_plain_value($)
+ - New attribute 'escape_char' allowing for proper escape of quote_chars
+ present in an identifier
+ - Deprecate { "" => \... } constructs
+ - Treat { -value => undef } as plain undef in all cases
+ - Explicitly throw on { -ident => undef }
+
revision 1.78 2014-05-28
----------------------------
- Fix parsing of binary ops to correctly take up only a single LHS
@@ -41,6 +41,7 @@ t/16no_sideeffects.t
t/20injection_guard.t
t/21op_ident.t
t/22op_value.t
+t/23_is_X_value.t
t/dbic/bulk-insert.t
t/dbic/no-repeats.t
t/dbic/show-progress.t
@@ -28,13 +28,16 @@ no_index:
package:
- DBIx::Class::Storage::Debug::PrettyPrint
requires:
+ Exporter: 5.57
Hash::Merge: 0.12
List::Util: 0
+ MRO::Compat: 0.12
Moo: 1.004002
Scalar::Util: 0
perl: 5.6.0
resources:
+ IRC: irc://irc.perl.org/#dbix-class
bugtracker: http://rt.cpan.org/NoAuth/Bugs.html?Dist=SQL-Abstract
license: http://dev.perl.org/licenses/
- repository: git://git.shadowcat.co.uk/dbsrgits/SQL-Abstract.git
-version: 1.78
+ repository: https://github.com/dbsrgits/sql-abstract/
+version: 1.79
@@ -8,8 +8,9 @@ perl_version '5.006';
name 'SQL-Abstract';
author 'Nathan Wiger <nate@wiger.org>';
resources 'license' => 'http://dev.perl.org/licenses/';
-resources 'repository' => 'git://git.shadowcat.co.uk/dbsrgits/SQL-Abstract.git';
+resources 'repository' => 'https://github.com/dbsrgits/sql-abstract/';
resources 'bugtracker' => 'http://rt.cpan.org/NoAuth/Bugs.html?Dist=SQL-Abstract';
+resources 'IRC' => 'irc://irc.perl.org/#dbix-class';
all_from 'lib/SQL/Abstract.pm';
@@ -17,6 +18,8 @@ dynamic_config 0;
requires 'List::Util' => 0;
requires 'Scalar::Util' => 0;
+requires 'Exporter' => 5.57;
+requires 'MRO::Compat' => 0.12;
requires 'Moo' => 1.004002;
requires 'Hash::Merge' => 0.12;
@@ -5,9 +5,19 @@ package SQL::Abstract::Tree;
BEGIN {
require warnings;
my $initial_fatal_bits = (${^WARNING_BITS}||'') & $warnings::DeadBits{all};
+
local $ENV{PERL_STRICTURES_EXTRA} = 0;
- require Moo; Moo->import;
- require Sub::Quote; Sub::Quote->import('quote_sub');
+ # load all of these now, so that lazy-loading does not escape
+ # the current PERL_STRICTURES_EXTRA setting
+ require Sub::Quote;
+ require Sub::Defer;
+ require Moo;
+ require Moo::Object;
+ require Method::Generate::Accessor;
+ require Method::Generate::Constructor;
+
+ Moo->import;
+ Sub::Quote->import('quote_sub');
${^WARNING_BITS} &= ( $initial_fatal_bits | ~ $warnings::DeadBits{all} );
}
# END pre-Moo2 import block
@@ -6,11 +6,28 @@ use Carp ();
use List::Util ();
use Scalar::Util ();
+use Exporter 'import';
+our @EXPORT_OK = qw(is_plain_value is_literal_value);
+
+BEGIN {
+ if ($] < 5.009_005) {
+ require MRO::Compat;
+ }
+ else {
+ require mro;
+ }
+
+ *SQL::Abstract::_ENV_::DETECT_AUTOGENERATED_STRINGIFICATION = $ENV{SQLA_ISVALUE_IGNORE_AUTOGENERATED_STRINGIFICATION}
+ ? sub () { 0 }
+ : sub () { 1 }
+ ;
+}
+
#======================================================================
# GLOBALS
#======================================================================
-our $VERSION = '1.78';
+our $VERSION = '1.79';
# This would confuse some packagers
$VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
@@ -58,6 +75,69 @@ sub puke (@) {
Carp::croak "[$func] Fatal: ", @_;
}
+sub is_literal_value ($) {
+ ref $_[0] eq 'SCALAR' ? [ ${$_[0]} ]
+ : ( ref $_[0] eq 'REF' and ref ${$_[0]} eq 'ARRAY' ) ? [ @${ $_[0] } ]
+ : (
+ ref $_[0] eq 'HASH' and keys %{$_[0]} == 1
+ and
+ defined $_[0]->{-ident} and ! length ref $_[0]->{-ident}
+ ) ? [ $_[0]->{-ident} ]
+ : undef;
+}
+
+# FIXME XSify - this can be done so much more efficiently
+sub is_plain_value ($) {
+ no strict 'refs';
+ ! length ref $_[0] ? \($_[0])
+ : (
+ ref $_[0] eq 'HASH' and keys %{$_[0]} == 1
+ and
+ exists $_[0]->{-value}
+ ) ? \($_[0]->{-value})
+ : (
+ # reuse @_ for even moar speedz
+ defined ( $_[1] = Scalar::Util::blessed $_[0] )
+ and
+ # deliberately not using Devel::OverloadInfo - the checks we are
+ # intersted in are much more limited than the fullblown thing, and
+ # this is a very hot piece of code
+ (
+ # simply using ->can('(""') can leave behind stub methods that
+ # break actually using the overload later (see L<perldiag/Stub
+ # found while resolving method "%s" overloading "%s" in package
+ # "%s"> and the source of overload::mycan())
+ #
+ # either has stringification which DBI SHOULD prefer out of the box
+ grep { *{ (qq[${_}::(""]) }{CODE} } @{ $_[2] = mro::get_linear_isa( $_[1] ) }
+ or
+ # has nummification or boolification, AND fallback is *not* disabled
+ (
+ SQL::Abstract::_ENV_::DETECT_AUTOGENERATED_STRINGIFICATION
+ and
+ (
+ grep { *{"${_}::(0+"}{CODE} } @{$_[2]}
+ or
+ grep { *{"${_}::(bool"}{CODE} } @{$_[2]}
+ )
+ and
+ (
+ # no fallback specified at all
+ ! ( ($_[3]) = grep { *{"${_}::()"}{CODE} } @{$_[2]} )
+ or
+ # fallback explicitly undef
+ ! defined ${"$_[3]::()"}
+ or
+ # explicitly true
+ !! ${"$_[3]::()"}
+ )
+ )
+ )
+ ) ? \($_[0])
+ : undef;
+}
+
+
#======================================================================
# NEW
@@ -417,9 +497,15 @@ sub _recurse_where {
my ($sql, @bind) = $self->$method($where, $logic);
- # DBIx::Class directly calls _recurse_where in scalar context, so
- # we must implement it, even if not in the official API
- return wantarray ? ($sql, @bind) : $sql;
+ # DBIx::Class used to call _recurse_where in scalar context
+ # something else might too...
+ if (wantarray) {
+ return ($sql, @bind);
+ }
+ else {
+ belch "Calling _recurse_where in scalar context is deprecated and will go away before 2.0";
+ return $sql;
+ }
}
@@ -439,7 +525,10 @@ sub _where_ARRAYREF {
my (@sql_clauses, @all_bind);
# need to use while() so can shift() for pairs
- while (my $el = shift @clauses) {
+ while (@clauses) {
+ my $el = shift @clauses;
+
+ $el = undef if (defined $el and ! length $el);
# switch according to kind of $el and get corresponding ($sql, @bind)
my ($sql, @bind) = $self->_SWITCH_refkind($el, {
@@ -457,10 +546,12 @@ sub _where_ARRAYREF {
SCALARREF => sub { ($$el); },
- SCALAR => sub {# top-level arrayref with scalars, recurse in pairs
- $self->_recurse_where({$el => shift(@clauses)})},
+ SCALAR => sub {
+ # top-level arrayref with scalars, recurse in pairs
+ $self->_recurse_where({$el => shift(@clauses)})
+ },
- UNDEF => sub {puke "not supported : UNDEF in arrayref" },
+ UNDEF => sub {puke "Supplying an empty left hand side argument is not supported in array-pairs" },
});
if ($sql) {
@@ -519,6 +610,15 @@ sub _where_HASHREF {
($s, @b);
}
else {
+ if (! length $k) {
+ if (is_literal_value ($v) ) {
+ belch 'Hash-pairs consisting of an empty string with a literal are deprecated, and will be removed in 2.0: use -and => [ $literal ] instead';
+ }
+ else {
+ puke "Supplying an empty left hand side argument is not supported in hash-pairs";
+ }
+ }
+
my $method = $self->_METHOD_FOR_refkind("_where_hashpair", $v);
$self->$method($k, $v);
}
@@ -667,8 +767,8 @@ sub _where_op_BOOL {
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)";
+ if (! defined $rhs or length ref $rhs) {
+ puke "-$op requires a single plain scalar argument (a quotable identifier)";
}
# in case we are called as a top level special op (no '=')
@@ -689,6 +789,14 @@ sub _where_op_VALUE {
# in case we are called as a top level special op (no '=')
my $lhs = shift;
+ # special-case NULL
+ if (! defined $rhs) {
+ return $lhs
+ ? $self->_convert($self->_quote($lhs)) . ' IS NULL'
+ : undef
+ ;
+ }
+
my @bind =
$self->_bindtype (
($lhs || $self->{_nested_func_lhs}),
@@ -765,6 +873,11 @@ sub _where_hashpair_HASHREF {
# so that -not_foo works correctly
$op =~ s/^not_/NOT /i;
+ # another retarded special case: foo => { $op => { -value => undef } }
+ if (ref $val eq 'HASH' and keys %$val == 1 and exists $val->{-value} and ! defined $val->{-value} ) {
+ $val = undef;
+ }
+
my ($sql, @bind);
# CASE: col-value logic modifiers
@@ -1262,10 +1375,11 @@ sub _quote {
else {
puke "Unsupported quote_char format: $_[0]->{quote_char}";
}
+ my $esc = $_[0]->{escape_char} || $r;
# parts containing * are naturally unquoted
return join( $_[0]->{name_sep}||'', map
- { $_ eq '*' ? $_ : $l . $_ . $r }
+ { $_ eq '*' ? $_ : do { (my $n = $_) =~ s/(\Q$esc\E|\Q$r\E)/$esc$1/g; $l . $n . $r } }
( $_[0]->{name_sep} ? split (/\Q$_[0]->{name_sep}\E/, $_[1] ) : $_[1] )
);
}
@@ -1624,7 +1738,7 @@ say something like this:
my %data = (
name => 'Bill',
- date_entered => \["to_date(?,'MM/DD/YYYY')", "03/02/2003"],
+ date_entered => \[ "to_date(?,'MM/DD/YYYY')", "03/02/2003" ],
);
The first value in the array is the actual SQL. Any other values are
@@ -1676,16 +1790,13 @@ Which you could then use in DBI code like so:
Easy, eh?
-=head1 FUNCTIONS
+=head1 METHODS
-The functions are simple. There's one for each major SQL operation,
+The methods are simple. There's one for every major SQL operation,
and a constructor you use first. The arguments are specified in a
-similar order to each function (table, then fields, then a where
+similar order for each method (table, then fields, then a where
clause) to try and simplify things.
-
-
-
=head2 new(option => 'value')
The C<new()> function takes a list of options and values, and returns
@@ -1828,7 +1939,7 @@ are or are not included. You could wrap that above C<for> loop in a simple
sub called C<bind_fields()> or something and reuse it repeatedly. You still
get a layer of abstraction over manual SQL specification.
-Note that if you set L</bindtype> to C<columns>, the C<\[$sql, @bind]>
+Note that if you set L</bindtype> to C<columns>, the C<\[ $sql, @bind ]>
construct (see L</Literal SQL with placeholders and bind values (subqueries)>)
will expect the bind values in this format.
@@ -1850,6 +1961,21 @@ that generates SQL like this:
Quoting is useful if you have tables or columns names that are reserved
words in your database's SQL dialect.
+=item escape_char
+
+This is the character that will be used to escape L</quote_char>s appearing
+in an identifier before it has been quoted.
+
+The paramter default in case of a single L</quote_char> character is the quote
+character itself.
+
+When opening-closing-style quoting is used (L</quote_char> is an arrayref)
+this parameter defaults to the B<closing (right)> L</quote_char>. Occurences
+of the B<opening (left)> L</quote_char> within the identifier are currently left
+untouched. The default for opening-closing-style quotes may change in future
+versions, thus you are B<strongly encouraged> to specify the escape character
+explicitly.
+
=item name_sep
This is the character that separates a table and column name. It is
@@ -2032,6 +2158,88 @@ Might give you:
You get the idea. Strings get their case twiddled, but everything
else remains verbatim.
+=head1 EXPORTABLE FUNCTIONS
+
+=head2 is_plain_value
+
+Determines if the supplied argument is a plain value as understood by this
+module:
+
+=over
+
+=item * The value is C<undef>
+
+=item * The value is a non-reference
+
+=item * The value is an object with stringification overloading
+
+=item * The value is of the form C<< { -value => $anything } >>
+
+=back
+
+On failure returns C<undef>, on sucess returns a B<scalar> reference
+to the original supplied argument.
+
+=over
+
+=item * Note
+
+The stringification overloading detection is rather advanced: it takes
+into consideration not only the presence of a C<""> overload, but if that
+fails also checks for enabled
+L<autogenerated versions of C<"">|overload/Magic Autogeneration>, based
+on either C<0+> or C<bool>.
+
+Unfortunately testing in the field indicates that this
+detection B<< may tickle a latent bug in perl versions before 5.018 >>,
+but only when very large numbers of stringifying objects are involved.
+At the time of writing ( Sep 2014 ) there is no clear explanation of
+the direct cause, nor is there a manageably small test case that reliably
+reproduces the problem.
+
+If you encounter any of the following exceptions in B<random places within
+your application stack> - this module may be to blame:
+
+ Operation "ne": no method found,
+ left argument in overloaded package <something>,
+ right argument in overloaded package <something>
+
+or perhaps even
+
+ Stub found while resolving method "???" overloading """" in package <something>
+
+If you fall victim to the above - please attempt to reduce the problem
+to something that could be sent to the L<SQL::Abstract developers
+|DBIx::Class/GETTING HELP/SUPPORT>
+(either publicly or privately). As a workaround in the meantime you can
+set C<$ENV{SQLA_ISVALUE_IGNORE_AUTOGENERATED_STRINGIFICATION}> to a true
+value, which will most likely eliminate your problem (at the expense of
+not being able to properly detect exotic forms of stringification).
+
+This notice and environment variable will be removed in a future version,
+as soon as the underlying problem is found and a reliable workaround is
+devised.
+
+=back
+
+=head2 is_literal_value
+
+Determines if the supplied argument is a literal value as understood by this
+module:
+
+=over
+
+=item * C<\$sql_string>
+
+=item * C<\[ $sql_string, @bind_values ]>
+
+=item * C<< { -ident => $plain_defined_string } >>
+
+=back
+
+On failure returns C<undef>, on sucess returns an B<array> reference
+containing the unpacked version of the supplied literal SQL and bind values.
+
=head1 WHERE CLAUSES
=head2 Introduction
@@ -2156,7 +2364,7 @@ Which would generate:
@bind = ('2', '5', 'nwiger');
If you want to include literal SQL (with or without bind values), just use a
-scalar reference or array reference as the value:
+scalar reference or reference to an arrayref as the value:
my %where = (
date_entered => { '>' => \["to_date(?, 'MM/DD/YYYY')", "11/26/2008"] },
@@ -2165,7 +2373,7 @@ scalar reference or array reference as the value:
Which would generate:
- $stmt = "WHERE date_entered > "to_date(?, 'MM/DD/YYYY') AND date_expires < now()";
+ $stmt = "WHERE date_entered > to_date(?, 'MM/DD/YYYY') AND date_expires < now()";
@bind = ('11/26/2008');
@@ -2179,7 +2387,7 @@ this (notice the C<AND>):
Because, in Perl you I<can't> do this:
- priority => { '!=', 2, '!=', 1 }
+ priority => { '!=' => 2, '!=' => 1 }
As the second C<!=> key will obliterate the first. The solution
is to use the special C<-modifier> form inside an arrayref:
@@ -2371,10 +2579,10 @@ to change the logic inside :
That would yield:
- WHERE ( user = ? AND (
- ( workhrs > ? AND geo = ? )
- OR ( workhrs < ? OR geo = ? )
- ) )
+ $stmt = "WHERE ( user = ?
+ AND ( ( workhrs > ? AND geo = ? )
+ OR ( workhrs < ? OR geo = ? ) ) )";
+ @bind = ('nwiger', '20', 'ASIA', '50', 'EURO');
=head3 Algebraic inconsistency, for historical reasons
@@ -2495,7 +2703,7 @@ not so common, but perfectly legal Perl). For example, to find a date
in Postgres you can use something like this:
my %where = (
- date_column => \[q/= date '2008-09-30' - ?::integer/, 10/]
+ date_column => \[ "= date '2008-09-30' - ?::integer", 10 ]
)
This would create:
@@ -2504,15 +2712,16 @@ This would create:
@bind = ('10');
Note that you must pass the bind values in the same format as they are returned
-by L</where>. That means that if you set L</bindtype> to C<columns>, you must
-provide the bind values in the C<< [ column_meta => value ] >> format, where
-C<column_meta> is an opaque scalar value; most commonly the column name, but
-you can use any scalar value (including references and blessed references),
-L<SQL::Abstract> will simply pass it through intact. So if C<bindtype> is set
-to C<columns> the above example will look like:
+by L<where|/where(\%where, \@order)>. This means that if you set L</bindtype>
+to C<columns>, you must provide the bind values in the
+C<< [ column_meta => value ] >> format, where C<column_meta> is an opaque
+scalar value; most commonly the column name, but you can use any scalar value
+(including references and blessed references), L<SQL::Abstract> will simply
+pass it through intact. So if C<bindtype> is set to C<columns> the above
+example will look like:
my %where = (
- date_column => \[q/= date '2008-09-30' - ?::integer/, [ dummy => 10 ]/]
+ date_column => \[ "= date '2008-09-30' - ?::integer", [ {} => 10 ] ]
)
Literal SQL is especially useful for nesting parenthesized clauses in the
@@ -2719,14 +2928,14 @@ 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<SQL::Abstract/> object as:
+L<SQL::Abstract> 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
+ $op is the part that matched the handler regex
$arg is the RHS
When supplied with a coderef, it is called as:
@@ -2795,7 +3004,7 @@ 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<SQL::Abstract/> object as:
+L<SQL::Abstract> object as:
$self->$method_name ($op, $arg)
@@ -2876,13 +3085,27 @@ a fast interface to returning and formatting data. I frequently
use these three modules together to write complex database query
apps in under 50 lines.
-=head1 REPO
+=head1 HOW TO CONTRIBUTE
+
+Contributions are always welcome, in all usable forms (we especially
+welcome documentation improvements). The delivery methods include git-
+or unified-diff formatted patches, GitHub pull requests, or plain bug
+reports either via RT or the Mailing list. Contributors are generally
+granted full access to the official repository after their first several
+patches pass successful review.
+
+This project is maintained in a git repository. The code and related tools are
+accessible at the following locations:
=over
-=item * gitweb: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/SQL-Abstract.git>
+=item * Official repo: L<git://git.shadowcat.co.uk/dbsrgits/SQL-Abstract.git>
+
+=item * Official gitweb: L<http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/SQL-Abstract.git>
+
+=item * GitHub mirror: L<https://github.com/dbsrgits/sql-abstract>
-=item * git: L<git://git.shadowcat.co.uk/dbsrgits/SQL-Abstract.git>
+=item * Authorized committers: L<ssh://dbsrgits@git.shadowcat.co.uk/SQL-Abstract.git>
=back
@@ -2903,7 +3126,7 @@ The main changes are :
=item *
-support for literal SQL through the C<< \ [$sql, bind] >> syntax.
+support for literal SQL through the C<< \ [ $sql, @bind ] >> syntax.
=item *
@@ -559,6 +559,21 @@ my @tests = (
bind => [],
warns => qr/\QSupplying an undefined argument to 'NOT LIKE' is deprecated/,
},
+ {
+ func => 'select',
+ args => ['`test``table`', ['`test``column`']],
+ stmt => 'SELECT `test``column` FROM `test``table`',
+ stmt_q => 'SELECT ```test````column``` FROM ```test````table```',
+ bind => [],
+ },
+ {
+ func => 'select',
+ args => ['`test\\`table`', ['`test`\\column`']],
+ stmt => 'SELECT `test`\column` FROM `test\`table`',
+ stmt_q => 'SELECT `\`test\`\\\\column\`` FROM `\`test\\\\\`table\``',
+ esc => '\\',
+ bind => [],
+ },
);
# check is( not) => undef
@@ -644,14 +659,139 @@ for my $op ( qw(like rlike not_like not_rlike), 'not like', 'not rlike', 'is lik
} for ('', '-'); # with and without -
}
+# check emtpty-lhs in a hashpair and arraypair
+for my $lhs (undef, '') {
+ no warnings 'uninitialized';
+
+##
+## hard exceptions - never worked
+ for my $where_arg (
+ ( map { $_, { @$_ } }
+ [ $lhs => "foo" ],
+ [ $lhs => { "=" => "bozz" } ],
+ [ $lhs => { "=" => \"bozz" } ],
+ [ $lhs => { -max => \"bizz" } ],
+ ),
+ [ -and => { $lhs => "baz" }, bizz => "buzz" ],
+ [ foo => "bar", { $lhs => "baz" }, bizz => "buzz" ],
+ { foo => "bar", -or => { $lhs => "baz" } },
+
+ # the hashref forms of these work sadly - check for warnings below
+ { foo => "bar", -and => [ $lhs => \"baz" ], bizz => "buzz" },
+ { foo => "bar", -or => [ $lhs => \"baz" ], bizz => "buzz" },
+ [ foo => "bar", [ $lhs => \"baz" ], bizz => "buzz" ],
+ [ foo => "bar", $lhs => \"baz", bizz => "buzz" ],
+ [ foo => "bar", $lhs => \["baz"], bizz => "buzz" ],
+ [ $lhs => \"baz" ],
+ [ $lhs => \["baz"] ],
+
+ # except for this one, that is automagically arrayified
+ { foo => "bar", -or => { $lhs => \"baz" }, bizz => "buzz" },
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ throws => qr/\QSupplying an empty left hand side argument is not supported/,
+ };
+ }
+
+##
+## deprecations - sorta worked, likely abused by folks
+ for my $where_arg (
+ # the arrayref forms of this never worked and throw above
+ { foo => "bar", -and => { $lhs => \"baz" }, bizz => "buzz" },
+ { foo => "bar", $lhs => \"baz", bizz => "buzz" },
+ { foo => "bar", $lhs => \["baz"], bizz => "buzz" },
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ stmt => 'WHERE baz AND bizz = ? AND foo = ?',
+ stmt_q => 'WHERE baz AND `bizz` = ? AND `foo` = ?',
+ bind => [qw( buzz bar )],
+ warns => qr/\QHash-pairs consisting of an empty string with a literal are deprecated/,
+ };
+ }
+
+ for my $where_arg (
+ { $lhs => \"baz" },
+ { $lhs => \["baz"] },
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ stmt => 'WHERE baz',
+ stmt_q => 'WHERE baz',
+ bind => [],
+ warns => qr/\QHash-pairs consisting of an empty string with a literal are deprecated/,
+ }
+ }
+}
+
+# check false lhs, silly but possible
+{
+ for my $where_arg (
+ [ { 0 => "baz" }, bizz => "buzz", foo => "bar" ],
+ [ -or => { foo => "bar", -or => { 0 => "baz" }, bizz => "buzz" } ],
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ stmt => 'WHERE 0 = ? OR bizz = ? OR foo = ?',
+ stmt_q => 'WHERE `0` = ? OR `bizz` = ? OR `foo` = ?',
+ bind => [qw( baz buzz bar )],
+ };
+ }
+
+ for my $where_arg (
+ { foo => "bar", -and => [ 0 => \"= baz" ], bizz => "buzz" },
+ { foo => "bar", -or => [ 0 => \"= baz" ], bizz => "buzz" },
+
+ { foo => "bar", -and => { 0 => \"= baz" }, bizz => "buzz" },
+ { foo => "bar", -or => { 0 => \"= baz" }, bizz => "buzz" },
+
+ { foo => "bar", 0 => \"= baz", bizz => "buzz" },
+ { foo => "bar", 0 => \["= baz"], bizz => "buzz" },
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ stmt => 'WHERE 0 = baz AND bizz = ? AND foo = ?',
+ stmt_q => 'WHERE `0` = baz AND `bizz` = ? AND `foo` = ?',
+ bind => [qw( buzz bar )],
+ };
+ }
+
+ for my $where_arg (
+ [ -and => [ 0 => \"= baz" ], bizz => "buzz", foo => "bar" ],
+ [ -or => [ 0 => \"= baz" ], bizz => "buzz", foo => "bar" ],
+ [ 0 => \"= baz", bizz => "buzz", foo => "bar" ],
+ [ 0 => \["= baz"], bizz => "buzz", foo => "bar" ],
+ ) {
+ push @tests, {
+ func => 'where',
+ args => [ $where_arg ],
+ stmt => 'WHERE 0 = baz OR bizz = ? OR foo = ?',
+ stmt_q => 'WHERE `0` = baz OR `bizz` = ? OR `foo` = ?',
+ bind => [qw( buzz bar )],
+ };
+ }
+}
+
for my $t (@tests) {
my $new = $t->{new} || {};
for my $quoted (0, 1) {
- my $maker = SQL::Abstract->new(%$new, $quoted
- ? (quote_char => '`', name_sep => '.')
- : ()
+ my $maker = SQL::Abstract->new(
+ %$new,
+ ($quoted ? (
+ quote_char => '`',
+ name_sep => '.',
+ ( $t->{esc} ? (
+ escape_char => $t->{esc},
+ ) : ())
+ ) : ())
);
my($stmt, @bind);
@@ -21,10 +21,10 @@ Test -and -or and -nest modifiers, assuming the following:
* Modifiers are respected in both hashrefs and arrayrefs (with the obvious
limitation of one modifier type per hahsref)
- * When in condition context i.e. where => { -or { a = 1 } }, each modifier
+ * When in condition context i.e. where => { -or => { a = 1 } }, each modifier
affects only the immediate element following it.
* When in column multi-condition context i.e.
- where => { x => { '!=', [-and, [qw/1 2 3/]] } }, a modifier affects the
+ where => { x => { '!=', [-and => [qw/1 2 3/]] } }, a modifier affects the
OUTER ARRAYREF if and only if it is the first element of said ARRAYREF
=cut
@@ -2,6 +2,7 @@ use strict;
use warnings;
use Test::More;
+use Test::Exception;
use SQL::Abstract;
use SQL::Abstract::Test import => [qw/is_same_sql_bind/];
@@ -12,6 +13,10 @@ for my $q ('', '"') {
name_sep => $q ? '.' : '',
);
+ throws_ok {
+ $sql_maker->where({ foo => { -ident => undef } })
+ } qr/-ident requires a single plain scalar argument/;
+
my ($sql, @bind) = $sql_maker->select ('artist', '*', { 'artist.name' => { -ident => 'artist.pseudonym' } } );
is_same_sql_bind (
$sql,
@@ -40,6 +40,38 @@ for my $col_btype (0,1) {
)
],
);
+
+ {
+ local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /Supplying an undefined argument to '(?:NOT )?LIKE'/ };
+
+ ($sql, @bind) = $sql_maker->where ({
+ c1 => undef,
+ c2 => { -value => undef },
+ c3 => { '=' => { -value => undef } },
+ c4 => { '!=' => { -value => undef } },
+ c5 => { '<>' => { -value => undef } },
+ c6 => { '-like' => { -value => undef } },
+ c7 => { '-not_like' => { -value => undef } },
+ c8 => { 'is' => { -value => undef } },
+ c9 => { 'is not' => { -value => undef } },
+ });
+
+ is_same_sql_bind (
+ $sql,
+ \@bind,
+ "WHERE ${q}c1${q} IS NULL
+ AND ${q}c2${q} IS NULL
+ AND ${q}c3${q} IS NULL
+ AND ${q}c4${q} IS NOT NULL
+ AND ${q}c5${q} IS NOT NULL
+ AND ${q}c6${q} IS NULL
+ AND ${q}c7${q} IS NOT NULL
+ AND ${q}c8${q} IS NULL
+ AND ${q}c9${q} IS NOT NULL
+ ",
+ [],
+ );
+ }
}}
done_testing;
@@ -0,0 +1,252 @@
+use warnings;
+use strict;
+
+use Test::More;
+use Test::Exception;
+use Scalar::Util 'refaddr';
+use Storable 'nfreeze';
+
+BEGIN { $ENV{SQLA_ISVALUE_IGNORE_AUTOGENERATED_STRINGIFICATION} = 0 }
+
+use SQL::Abstract qw(is_plain_value is_literal_value);
+
+# fallback setting is inheriting starting p5 50853fa9 (run up to 5.17.0)
+use constant OVERLOAD_FALLBACK_INHERITS => ( ($] < 5.017) ? 0 : 1 );
+use constant STRINGIFIER_CAN_RETURN_IVS => ( ($] < 5.008) ? 0 : 1 );
+
+{
+ package # hideee
+ SQLATest::SillyBool;
+
+ use overload
+ # *DELIBERATELY* unspecified
+ #fallback => 1,
+ bool => sub { ${$_[0]} },
+ ;
+
+ package # hideee
+ SQLATest::SillyBool::Subclass;
+
+ our @ISA = 'SQLATest::SillyBool';
+}
+
+{
+ package # hideee
+ SQLATest::SillyInt;
+
+ use overload
+ # *DELIBERATELY* unspecified
+ #fallback => 1,
+ '0+' => sub { ${$_[0]} },
+ ;
+
+ package # hideee
+ SQLATest::SillyInt::Subclass;
+
+ our @ISA = 'SQLATest::SillyInt';
+}
+
+{
+ package # hideee
+ SQLATest::SillierInt;
+
+ use overload
+ fallback => 0,
+ ;
+
+ package # hideee
+ SQLATest::SillierInt::Subclass;
+
+ use overload
+ '0+' => sub { ${$_[0]} },
+ '+' => sub { ${$_[0]} + $_[1] },
+ ;
+
+ our @ISA = 'SQLATest::SillierInt';
+}
+
+{
+ package # hideee
+ SQLATest::AnalInt;
+
+ use overload
+ fallback => 0,
+ '0+' => sub { ${$_[0]} },
+ ;
+
+ package # hideee
+ SQLATest::AnalInt::Subclass;
+
+ use overload
+ '0+' => sub { ${$_[0]} },
+ ;
+
+ our @ISA = 'SQLATest::AnalInt';
+}
+
+{
+ package # hidee
+ SQLATest::ReasonableInt;
+
+ # make it match JSON::PP::Boolean
+ use overload
+ '0+' => sub { ${$_[0]} },
+ '++' => sub { $_[0] = ${$_[0]} + 1 },
+ '--' => sub { $_[0] = ${$_[0]} - 1 },
+ fallback => 1,
+ ;
+
+ package # hideee
+ SQLATest::ReasonableInt::Subclass;
+
+ our @ISA = 'SQLATest::ReasonableInt';
+}
+
+{
+ package # hidee
+ SQLATest::ReasonableString;
+
+ # somewhat like DateTime
+ use overload
+ 'fallback' => 1,
+ '""' => sub { "${$_[0]}" },
+ '-' => sub { ${$_[0]} - $_[1] },
+ '+' => sub { ${$_[0]} + $_[1] },
+ ;
+
+ package # hideee
+ SQLATest::ReasonableString::Subclass;
+
+ our @ISA = 'SQLATest::ReasonableString';
+}
+
+for my $case (
+ { class => 'SQLATest::SillyBool', can_math => 0, should_str => 1 },
+ { class => 'SQLATest::SillyBool::Subclass', can_math => 0, should_str => 1 },
+ { class => 'SQLATest::SillyInt', can_math => 0, should_str => 1 },
+ { class => 'SQLATest::SillyInt::Subclass', can_math => 0, should_str => 1 },
+ { class => 'SQLATest::SillierInt', can_math => 0, should_str => 0 },
+ { class => 'SQLATest::SillierInt::Subclass',can_math => 1, should_str => (OVERLOAD_FALLBACK_INHERITS ? 0 : 1) },
+ { class => 'SQLATest::AnalInt', can_math => 0, should_str => 0 },
+ { class => 'SQLATest::AnalInt::Subclass', can_math => 0, should_str => (OVERLOAD_FALLBACK_INHERITS ? 0 : 1) },
+ { class => 'SQLATest::ReasonableInt', can_math => 1, should_str => 1 },
+ { class => 'SQLATest::ReasonableInt::Subclass', can_math => 1, should_str => 1 },
+ { class => 'SQLATest::ReasonableString', can_math => 1, should_str => 1 },
+ { class => 'SQLATest::ReasonableString::Subclass',can_math => 1, should_str => 1 },
+) {
+
+ my $num = bless( \do { my $foo = 42 }, $case->{class} );
+
+ my $can_str = eval { "$num" eq 42 } || 0;
+
+ ok (
+ !($can_str xor $case->{should_str}),
+ "should_str setting for $case->{class} matches perl behavior",
+ ) || diag explain { %$case, can_str => $can_str };
+
+ my $can_math = eval { ($num + 1) == 43 } ? 1 : 0;
+
+ ok (
+ !($can_math xor $case->{can_math}),
+ "can_math setting for $case->{class} matches perl behavior",
+ ) || diag explain { %$case, actual_can_math => $can_math };
+
+ my $can_cmp = eval { my $dum = ($num eq "nope"); 1 } || 0;
+
+ for (1,2) {
+
+ if ($can_str) {
+
+ ok $num, 'bool ctx works';
+
+ if (STRINGIFIER_CAN_RETURN_IVS and $can_cmp) {
+ is_deeply(
+ is_plain_value $num,
+ \$num,
+ "stringification detected on $case->{class}",
+ ) || diag explain $case;
+ }
+ else {
+ # is_deeply does not do nummify/stringify cmps properly
+ # but we can always compare the ice
+ ok(
+ ( nfreeze( is_plain_value $num ) eq nfreeze( \$num ) ),
+ "stringification without cmp capability detected on $case->{class}"
+ ) || diag explain $case;
+ }
+
+ is (
+ refaddr( ${is_plain_value($num)} ),
+ refaddr $num,
+ "Same reference (blessed object) returned",
+ );
+ }
+ else {
+ is( is_plain_value($num), undef, "non-stringifiable $case->{class} object detected" )
+ || diag explain $case;
+ }
+
+ if ($case->{can_math}) {
+ is ($num+1, 43);
+ }
+ }
+}
+
+lives_ok {
+ my $num = bless( \do { my $foo = 23 }, 'SQLATest::ReasonableInt' );
+ cmp_ok(++$num, '==', 24, 'test overloaded object compares correctly');
+ cmp_ok(--$num, 'eq', 23, 'test overloaded object compares correctly');
+ is_deeply(
+ is_plain_value $num,
+ \23,
+ 'fallback stringification detected'
+ );
+ cmp_ok(--$num, 'eq', 22, 'test overloaded object compares correctly');
+ cmp_ok(++$num, '==', 23, 'test overloaded object compares correctly');
+} 'overload testing lives';
+
+
+is_deeply
+ is_plain_value { -value => [] },
+ \[],
+ '-value recognized'
+;
+
+for ([], {}, \'') {
+ is
+ is_plain_value $_,
+ undef,
+ 'nonvalues correctly recognized'
+ ;
+}
+
+for (undef, { -value => undef }) {
+ is_deeply
+ is_plain_value $_,
+ \undef,
+ 'NULL -value recognized'
+ ;
+}
+
+is_deeply
+ is_literal_value { -ident => 'foo' },
+ [ 'foo' ],
+ '-ident recognized as literal'
+;
+
+is_deeply
+ is_literal_value \[ 'sql', 'bind1', [ {} => 'bind2' ] ],
+ [ 'sql', 'bind1', [ {} => 'bind2' ] ],
+ 'literal correctly unpacked'
+;
+
+
+for ([], {}, \'', undef) {
+ is
+ is_literal_value { -ident => $_ },
+ undef,
+ 'illegal -ident does not trip up detection'
+ ;
+}
+
+done_testing;