Add SQLMaker methods for matching and unquoting quoted identifiers
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / SQLMaker.pm
index 0db6ef2..0d27ec9 100644 (file)
@@ -44,14 +44,56 @@ use namespace::clean;
 
 __PACKAGE__->mk_group_accessors (simple => qw/quote_char name_sep limit_dialect/);
 
+sub _quoting_enabled {
+  ( defined $_[0]->{quote_char} and length $_[0]->{quote_char} ) ? 1 : 0
+}
+
 # for when I need a normalized l/r pair
 sub _quote_chars {
+
+  # in case we are called in the old !!$sm->_quote_chars fashion
+  return () if !wantarray and ( ! defined $_[0]->{quote_char} or ! length $_[0]->{quote_char} );
+
   map
     { defined $_ ? $_ : '' }
     ( ref $_[0]->{quote_char} ? (@{$_[0]->{quote_char}}) : ( ($_[0]->{quote_char}) x 2 ) )
   ;
 }
 
+sub _escape_char {
+  $_[0]->{escape_char} || ($_[0]->_quote_chars)[1] || '';
+}
+
+sub _unquote {
+  my ($self, $value) = @_;
+
+  return $value unless defined $value;
+
+  my ($l, $r, $e) = map { quotemeta $_ } $self->_quote_chars, $self->_escape_char;
+
+  # no quoting, all bets are off
+  return $value unless length $e;
+
+  my $re = $self->_quoted_ident_re($l, $r, $e);
+
+  if ($value =~ /\A$re\z/) {
+    $value =~ s/\A$l//;
+    $value =~ s/$r\z//;
+    $value =~ s/( $e [$e$r] )/substr($1, 1)/gex;
+    return $value;
+  }
+  else {
+    # not a quoted value, assume it's an identifier
+    return $value;
+  }
+}
+
+sub _quoted_ident_re {
+  my $self = shift;
+  my ($l, $r, $e) = @_ ? @_ : map { quotemeta $_ } $self->_quote_chars, $self->_escape_char;
+  return qr/ $l (?: [^$e$r] | $e [$e$r] )+ $r /x;
+}
+
 # FIXME when we bring in the storage weaklink, check its schema
 # weaklink and channel through $schema->throw_exception
 sub throw_exception { DBIx::Class::Exception->throw($_[1]) }
@@ -110,7 +152,7 @@ sub select {
   my ($self, $table, $fields, $where, $rs_attrs, $limit, $offset) = @_;
 
 
-  $fields = $self->_recurse_fields($fields);
+  ($fields, @{$self->{select_bind}}) = $self->_recurse_fields($fields);
 
   if (defined $offset) {
     $self->throw_exception('A supplied offset must be a non-negative integer')
@@ -231,42 +273,47 @@ sub _recurse_fields {
   return $$fields if $ref eq 'SCALAR';
 
   if ($ref eq 'ARRAY') {
-    return join(', ', map { $self->_recurse_fields($_) } @$fields);
+    my (@select, @bind);
+    for my $field (@$fields) {
+      my ($select, @new_bind) = $self->_recurse_fields($field);
+      push @select, $select;
+      push @bind, @new_bind;
+    }
+    return (join(', ', @select), @bind);
   }
   elsif ($ref eq 'HASH') {
     my %hash = %$fields;  # shallow copy
 
     my $as = delete $hash{-as};   # if supplied
 
-    my ($func, $args, @toomany) = %hash;
+    my ($func, $rhs, @toomany) = %hash;
 
     # there should be only one pair
     if (@toomany) {
       $self->throw_exception( "Malformed select argument - too many keys in hash: " . join (',', keys %$fields ) );
     }
 
-    if (lc ($func) eq 'distinct' && ref $args eq 'ARRAY' && @$args > 1) {
+    if (lc ($func) eq 'distinct' && ref $rhs eq 'ARRAY' && @$rhs > 1) {
       $self->throw_exception (
         'The select => { distinct => ... } syntax is not supported for multiple columns.'
-       .' Instead please use { group_by => [ qw/' . (join ' ', @$args) . '/ ] }'
-       .' or { select => [ qw/' . (join ' ', @$args) . '/ ], distinct => 1 }'
+       .' Instead please use { group_by => [ qw/' . (join ' ', @$rhs) . '/ ] }'
+       .' or { select => [ qw/' . (join ' ', @$rhs) . '/ ], distinct => 1 }'
       );
     }
 
+    my ($rhs_sql, @rhs_bind) = $self->_recurse_fields($rhs);
     my $select = sprintf ('%s( %s )%s',
       $self->_sqlcase($func),
-      $self->_recurse_fields($args),
+      $rhs_sql,
       $as
         ? sprintf (' %s %s', $self->_sqlcase('as'), $self->_quote ($as) )
         : ''
     );
 
-    return $select;
+    return ($select, @rhs_bind);
   }
-  # Is the second check absolutely necessary?
   elsif ( $ref eq 'REF' and ref($$fields) eq 'ARRAY' ) {
-    push @{$self->{select_bind}}, @{$$fields}[1..$#$$fields];
-    return $$fields->[0];
+    return @{$$fields};
   }
   else {
     $self->throw_exception( $ref . qq{ unexpected in _recurse_fields()} );
@@ -288,11 +335,9 @@ sub _parse_rs_attrs {
   my $sql = '';
 
   if ($arg->{group_by}) {
-    # horrible horrible, waiting for refactor
-    local $self->{select_bind};
-    if (my $g = $self->_recurse_fields($arg->{group_by}) ) {
-      $sql .= $self->_sqlcase(' group by ') . $g;
-      push @{$self->{group_bind} ||= []}, @{$self->{select_bind}||[]};
+    if ( my ($group_sql, @group_bind) = $self->_recurse_fields($arg->{group_by}) ) {
+      $sql .= $self->_sqlcase(' group by ') . $group_sql;
+      push @{$self->{group_bind}}, @group_bind;
     }
   }
 
@@ -512,7 +557,7 @@ sub _where_op_multicolumn_in {
 
   for ($lhs, $rhs) {
     $$_->[0] = "( $$_->[0] )"
-      unless $$_->[0] =~ /^ \s* \( .* \) \s* ^/xs;
+      unless $$_->[0] =~ /^ \s* \( .* \) \s* $/xs;
   }
 
   \[ join( ' IN ', shift @$$lhs, shift @$$rhs ), @$$lhs, @$$rhs ];