Well how about them apples?! Add temporary overload-madness escape hatch
[dbsrgits/SQL-Abstract.git] / lib / SQL / Abstract.pm
index 5cdc47f..08f6011 100644 (file)
@@ -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.75';
+our $VERSION  = '1.78';
 
 # 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
@@ -667,8 +747,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 +769,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 +853,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 +1355,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] )
   );
 }
@@ -1676,16 +1770,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 each 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 to 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
@@ -1850,6 +1941,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 +2138,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