identifier parsing to make mysql subselect wrapping work within literals
Matt S Trout [Sun, 3 Nov 2013 23:34:35 +0000 (23:34 +0000)]
lib/DBIx/Class/SQLMaker/Converter/MySQL.pm [new file with mode: 0644]
lib/DBIx/Class/SQLMaker/MySQL.pm

diff --git a/lib/DBIx/Class/SQLMaker/Converter/MySQL.pm b/lib/DBIx/Class/SQLMaker/Converter/MySQL.pm
new file mode 100644 (file)
index 0000000..727eef5
--- /dev/null
@@ -0,0 +1,83 @@
+package DBIx::Class::SQLMaker::Converter::MySQL;
+
+use Data::Query::ExprHelpers;
+use Moo;
+use namespace::clean;
+
+extends 'DBIx::Class::SQLMaker::Converter';
+
+foreach my $type (qw(update delete)) {
+  around "_${type}_to_dq" => sub {
+    my ($orig, $self) = (shift, shift);
+    $self->_mangle_mutation_dq($self->$orig(@_));
+  };
+}
+
+sub _mangle_mutation_dq {
+  my ($self, $dq) = @_;
+  my $target = $dq->{target};
+  my $target_name_re = do {
+    if (is_Identifier $target) {
+      join("\\.", map "(?:\`\Q$_\E\`|\Q$_\E)", @{$target->{elements}})
+    } elsif (
+      is_Literal $target
+      and $target->{literal}
+      and $target->{literal} =~ /^(?:\`([^`]+)\`|([\w\-]+))$/
+    ) {
+      map "\`\Q$_\E\`|\Q$_\E", (defined $1) ? $1 : $2;
+    } else {
+      undef
+    }
+  };
+  return $dq unless defined $target_name_re;
+  my $match_re = "SELECT(.*(?:FROM|JOIN)\\s+)${target_name_re}(.*)";
+  my $selectify = sub {
+    my ($before, $after, $values) = @_;
+    $before =~ s/FROM\s+(.*)//i;
+    my $from_before = $1;
+    return Select(
+      [ Literal('SQL' => $before) ],
+      Literal('SQL' => [
+        Literal('SQL' => $from_before),
+        $target,
+        Literal('SQL' => $after, $values)
+      ])
+    );
+  };
+  map_dq_tree {
+    if (is_Literal) {
+      if ($_->{literal} =~ /^${match_re}$/i) {
+        return \$selectify->($1, $2, $_->{values});
+      }
+      if ($_->{literal} =~ /\(\s*SELECT\s+/i) {
+        require Text::Balanced;
+        my $remain = $_->{literal};
+        my $before = '';
+        my @parts;
+        while ($remain =~ s/^(.*?)(\(\s*SELECT\s+.*)$/$2/i) {
+          $before .= $1;
+          (my ($select), $remain) = do {
+            # idiotic design - writes to $@ but *DOES NOT* throw exceptions
+            local $@;
+            Text::Balanced::extract_bracketed( $remain, '()', qr/[^\(]*/ );
+          };
+          return $_ unless $select; # balanced failed, give up
+          if ($select =~ /^\(\s*${match_re}\s*\)$/i) {
+            my $sel_dq = $selectify->($1, $2);
+            push @parts, Literal(SQL => "${before}("), $sel_dq;
+            $before = ')';
+          } else {
+            $before .= $select;
+          }
+        }
+        if (@parts) {
+          push @parts, Literal(SQL => $before.$remain, $_->{values});
+          return \Literal(SQL => \@parts);
+        }
+      }
+    }
+    $_
+  } $dq;
+};
+
+1;
index 1b16ca8..d3af351 100644 (file)
@@ -3,6 +3,10 @@ package # Hide from PAUSE
 
 use base qw( DBIx::Class::SQLMaker );
 
+sub _build_converter_class {
+  Module::Runtime::use_module('DBIx::Class::SQLMaker::Converter::MySQL');
+}
+
 sub _build_base_renderer_class {
   Module::Runtime::use_module('Data::Query::Renderer::SQL::MySQL');
 }