switch insert over to clauses system
[scpubgit/Q-Branch.git] / lib / SQL / Abstract.pm
index 6a8abaf..3016bc1 100644 (file)
@@ -137,6 +137,79 @@ sub is_plain_value ($) {
 # NEW
 #======================================================================
 
+our %Defaults = (
+  expand => {
+    not => '_expand_not',
+    bool => '_expand_bool',
+    and => '_expand_op_andor',
+    or => '_expand_op_andor',
+    nest => '_expand_nest',
+    bind => '_expand_bind',
+    in => '_expand_in',
+    not_in => '_expand_in',
+    row => '_expand_row',
+    between => '_expand_between',
+    not_between => '_expand_between',
+    op => '_expand_op',
+    (map +($_ => '_expand_op_is'), ('is', 'is_not')),
+    ident => '_expand_ident',
+    value => '_expand_value',
+    func => '_expand_func',
+    values => '_expand_values',
+  },
+  expand_op => {
+    'between' => '_expand_between',
+    'not_between' => '_expand_between',
+    'in' => '_expand_in',
+    'not_in' => '_expand_in',
+    'nest' => '_expand_nest',
+    (map +($_ => '_expand_op_andor'), ('and', 'or')),
+    (map +($_ => '_expand_op_is'), ('is', 'is_not')),
+    'ident' => '_expand_ident',
+    'value' => '_expand_value',
+  },
+  render => {
+    (map +($_, "_render_$_"), qw(op func bind ident literal row values)),
+  },
+  render_op => {
+    (map +($_ => '_render_op_between'), 'between', 'not_between'),
+    (map +($_ => '_render_op_in'), 'in', 'not_in'),
+    (map +($_ => '_render_unop_postfix'),
+      'is_null', 'is_not_null', 'asc', 'desc',
+    ),
+    (not => '_render_unop_paren'),
+    (map +($_ => '_render_op_andor'), qw(and or)),
+    ',' => '_render_op_multop',
+  },
+  clauses_of => {
+    delete => [ qw(target where returning) ],
+    update => [ qw(target set where returning) ],
+    insert => [ qw(target fields from returning) ],
+  },
+  expand_clause => {
+    'delete.from' => '_expand_delete_clause_target',
+    'update.update' => '_expand_update_clause_target',
+    'insert.into' => '_expand_insert_clause_target',
+    'insert.values' => '_expand_insert_clause_from',
+  },
+  render_clause => {
+    'delete.target' => '_render_delete_clause_target',
+    'update.target' => '_render_update_clause_target',
+    'insert.target' => '_render_insert_clause_target',
+    'insert.fields' => '_render_insert_clause_fields',
+    'insert.from' => '_render_insert_clause_from',
+  },
+);
+
+foreach my $stmt (keys %{$Defaults{clauses_of}}) {
+  $Defaults{expand}{$stmt} = '_expand_statement';
+  $Defaults{render}{$stmt} = '_render_statement';
+  foreach my $clause (@{$Defaults{clauses_of}{$stmt}}) {
+    $Defaults{expand_clause}{"${stmt}.${clause}"}
+      = "_expand_${stmt}_clause_${clause}";
+  }
+}
+
 sub new {
   my $self = shift;
   my $class = ref($self) || $self;
@@ -193,52 +266,9 @@ sub new {
 
   $opt{expand_unary} = {};
 
-  $opt{expand} = {
-    not => '_expand_not',
-    bool => '_expand_bool',
-    and => '_expand_op_andor',
-    or => '_expand_op_andor',
-    nest => '_expand_nest',
-    bind => '_expand_bind',
-    in => '_expand_in',
-    not_in => '_expand_in',
-    row => '_expand_row',
-    between => '_expand_between',
-    not_between => '_expand_between',
-    op => '_expand_op',
-    (map +($_ => '_expand_op_is'), ('is', 'is_not')),
-    ident => '_expand_ident',
-    value => '_expand_value',
-    func => '_expand_func',
-  };
-
-  $opt{expand_op} = {
-    'between' => '_expand_between',
-    'not_between' => '_expand_between',
-    'in' => '_expand_in',
-    'not_in' => '_expand_in',
-    'nest' => '_expand_nest',
-    (map +($_ => '_expand_op_andor'), ('and', 'or')),
-    (map +($_ => '_expand_op_is'), ('is', 'is_not')),
-    'ident' => '_expand_ident',
-    'value' => '_expand_value',
-  };
-
-  $opt{render} = {
-    (map +($_, "_render_$_"), qw(op func bind ident literal row)),
-    %{$opt{render}||{}}
-  };
-
-  $opt{render_op} = {
-    (map +($_ => '_render_op_between'), 'between', 'not_between'),
-    (map +($_ => '_render_op_in'), 'in', 'not_in'),
-    (map +($_ => '_render_unop_postfix'),
-      'is_null', 'is_not_null', 'asc', 'desc',
-    ),
-    (not => '_render_unop_paren'),
-    (map +($_ => '_render_op_andor'), qw(and or)),
-    ',' => '_render_op_multop',
-  };
+  foreach my $name (sort keys %Defaults) {
+    $opt{$name} = { %{$Defaults{$name}} };
+  }
 
   if ($opt{lazy_join_sql_parts}) {
     my $mod = Module::Runtime::use_module('SQL::Abstract::Parts');
@@ -268,25 +298,46 @@ sub _assert_pass_injection_guard {
 #======================================================================
 
 sub insert {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $data    = shift || return;
-  my $options = shift;
+  my ($self, $table, $data, $options) = @_;
 
-  my $fields;
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table;
+    } else {
+      my %clauses = (target => $table, values => $data, %{$options||{}});
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -insert => $stmt });
+  return wantarray ? @rendered : $rendered[0];
+}
 
-  my ($f_aqt, $v_aqt) = $self->_expand_insert_values($data);
+sub _expand_insert_clause_target {
+  +(target => $_[0]->_expand_maybe_list_expr($_[2], -ident));
+}
 
-  my @parts = ([ $self->_sqlcase('insert into').' '.$table ]);
-  push @parts, $self->render_aqt($f_aqt) if $f_aqt;
-  push @parts, [ $self->_sqlcase('values') ], $self->render_aqt($v_aqt);
+sub _expand_insert_clause_fields {
+  return +{ -row => [
+    $_[0]->_expand_maybe_list_expr($_[2], -ident)
+  ] } if ref($_[2]) eq 'ARRAY';
+  return $_[2]; # should maybe still expand somewhat?
+}
 
-  if ($options->{returning}) {
-    push @parts, [ $self->_insert_returning($options) ];
+sub _expand_insert_clause_from {
+  my ($self, undef, $data) = @_;
+  if (ref($data) eq 'HASH' and (keys(%$data))[0] =~ /^-/) {
+    return $self->expand_expr($data);
   }
+  return $data if ref($data) eq 'HASH' and $data->{-row};
+  my ($f_aqt, $v_aqt) = $self->_expand_insert_values($data);
+  return (
+    from => { -values => [ $v_aqt ] },
+    ($f_aqt ? (fields => $f_aqt) : ()),
+  );
+}
 
-  my ($sql, @bind) = @{ $self->join_query_parts(' ', @parts) };
-  return wantarray ? ($sql, @bind) : $sql;
+sub _expand_insert_clause_returning {
+  +(returning => $_[0]->_expand_maybe_list_expr($_[2], -ident));
 }
 
 sub _expand_insert_values {
@@ -319,10 +370,28 @@ sub _expand_insert_values {
   }
 }
 
+sub _render_insert_clause_fields {
+  return $_[0]->render_aqt($_[2]);
+}
+
+sub _render_insert_clause_target {
+  my ($self, undef, $from) = @_;
+  $self->join_query_parts(' ', $self->format_keyword('insert into'), $from);
+}
+
+sub _render_insert_clause_from {
+  return $_[0]->render_aqt($_[2], 1);
+}
+
 # So that subclasses can override INSERT ... RETURNING separately from
 # UPDATE and DELETE (e.g. DBIx::Class::SQLMaker::Oracle does this)
 sub _insert_returning { shift->_returning(@_) }
 
+sub _redispatch_returning {
+  my ($self, $type, undef, $returning) = @_;
+  [ $self->${\"_${type}_returning"}({ returning => $returning }) ];
+}
+
 sub _returning {
   my ($self, $options) = @_;
 
@@ -365,35 +434,28 @@ sub _expand_insert_value {
 # UPDATE methods
 #======================================================================
 
-
 sub update {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $data    = shift || return;
-  my $where   = shift;
-  my $options = shift;
+  my ($self, $table, $set, $where, $options) = @_;
 
-  # first build the 'SET' part of the sql statement
-  puke "Unsupported data type specified to \$sql->update"
-    unless ref $data eq 'HASH';
-
-  my ($sql, @all_bind) = $self->_update_set_values($data);
-  $sql = $self->_sqlcase('update ') . $table . $self->_sqlcase(' set ')
-          . $sql;
-
-  if ($where) {
-    my($where_sql, @where_bind) = $self->where($where);
-    $sql .= $where_sql;
-    push @all_bind, @where_bind;
-  }
-
-  if ($options->{returning}) {
-    my ($returning_sql, @returning_bind) = $self->_update_returning($options);
-    $sql .= $returning_sql;
-    push @all_bind, @returning_bind;
-  }
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table
+    } else {
+      my %clauses;
+      @clauses{qw(target set where)} = ($table, $set, $where);
+      puke "Unsupported data type specified to \$sql->update"
+        unless ref($clauses{set}) eq 'HASH';
+      @clauses{keys %$options} = values %$options;
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -update => $stmt });
+  return wantarray ? @rendered : $rendered[0];
+}
 
-  return wantarray ? ($sql, @all_bind) : $sql;
+sub _render_update_clause_target {
+  my ($self, undef, $target) = @_;
+  $self->join_query_parts(' ', $self->format_keyword('update'), $target);
 }
 
 sub _update_set_values {
@@ -428,6 +490,24 @@ sub _expand_update_set_values {
   ] );
 }
 
+sub _expand_update_clause_target {
+  my ($self, undef, $target) = @_;
+  +(target => $self->_expand_maybe_list_expr($target, -ident));
+}
+
+sub _expand_update_clause_set {
+  return $_[2] if ref($_[2]) eq 'HASH' and ($_[2]->{-op}||[''])->[0] eq ',';
+  +(set => $_[0]->_expand_update_set_values($_[1], $_[2]));
+}
+
+sub _expand_update_clause_where {
+  +(where => $_[0]->expand_expr($_[2]));
+}
+
+sub _expand_update_clause_returning {
+  +(returning => $_[0]->_expand_maybe_list_expr($_[2], -ident));
+}
+
 # So that subclasses can override UPDATE ... RETURNING separately from
 # INSERT and DELETE
 sub _update_returning { shift->_returning(@_) }
@@ -470,30 +550,39 @@ sub _select_fields {
 # DELETE
 #======================================================================
 
-
 sub delete {
-  my $self    = shift;
-  my $table   = $self->_table(shift);
-  my $where   = shift;
-  my $options = shift;
+  my ($self, $table, $where, $options) = @_;
 
-  my($where_sql, @bind) = $self->where($where);
-  my $sql = $self->_sqlcase('delete from ') . $table . $where_sql;
-
-  if ($options->{returning}) {
-    my ($returning_sql, @returning_bind) = $self->_delete_returning($options);
-    $sql .= $returning_sql;
-    push @bind, @returning_bind;
-  }
-
-  return wantarray ? ($sql, @bind) : $sql;
+  my $stmt = do {
+    if (ref($table) eq 'HASH') {
+      $table;
+    } else {
+      my %clauses = (target => $table, where => $where, %{$options||{}});
+      \%clauses;
+    }
+  };
+  my @rendered = $self->render_statement({ -delete => $stmt });
+  return wantarray ? @rendered : $rendered[0];
 }
 
 # So that subclasses can override DELETE ... RETURNING separately from
 # INSERT and UPDATE
 sub _delete_returning { shift->_returning(@_) }
 
+sub _expand_delete_clause_target {
+  +(target => $_[0]->_expand_maybe_list_expr($_[2], -ident));
+}
+
+sub _expand_delete_clause_where { +(where => $_[0]->expand_expr($_[2])); }
 
+sub _expand_delete_clause_returning {
+  +(returning => $_[0]->_expand_maybe_list_expr($_[2], -ident));
+}
+
+sub _render_delete_clause_target {
+   my ($self, undef, $from) = @_;
+   $self->join_query_parts(' ', $self->format_keyword('delete from'), $from);
+}
 
 #======================================================================
 # WHERE: entry point
@@ -532,11 +621,12 @@ sub expand_expr {
 }
 
 sub render_aqt {
-  my ($self, $aqt) = @_;
+  my ($self, $aqt, $top_level) = @_;
   my ($k, $v, @rest) = %$aqt;
   die "No" if @rest;
   die "Not a node type: $k" unless $k =~ s/^-//;
   if (my $meth = $self->{render}{$k}) {
+    local our $Render_Top_Level = $top_level;
     return $self->$meth($k, $v);
   }
   die "notreached: $k";
@@ -549,6 +639,61 @@ sub render_expr {
   ) };
 }
 
+sub render_statement {
+  my ($self, $expr, $default_scalar_to) = @_;
+  @{$self->render_aqt(
+    $self->expand_expr($expr, $default_scalar_to), 1
+  )};
+}
+
+sub _expand_statement {
+  my ($self, $type, $args) = @_;
+  my $ec = $self->{expand_clause};
+  if ($args->{_}) {
+    $args = { %$args };
+    $args->{$type} = delete $args->{_}
+  }
+  return +{ "-${type}" => +{
+    map {
+      my $val = $args->{$_};
+      if (defined($val) and my $exp = $ec->{"${type}.$_"}) {
+        if ((my (@exp) = $self->$exp($_ => $val)) == 1) {
+          ($_ => $exp[0])
+        } else {
+          @exp
+        }
+      } else {
+        ($_ => $self->expand_expr($val))
+      }
+    } sort keys %$args
+  } };
+}
+
+sub _render_statement {
+  my ($self, $type, $args) = @_;
+  my @parts;
+  foreach my $clause (@{$self->{clauses_of}{$type}}) {
+    next unless my $clause_expr = $args->{$clause};
+    my $part = do {
+      if (my $rdr = $self->{render_clause}{"${type}.${clause}"}) {
+        $self->$rdr($clause, $clause_expr);
+      } else {
+        my $r = $self->render_aqt($clause_expr, 1);
+        next unless defined $r->[0] and length $r->[0];
+        $self->join_query_parts(' ',
+          $self->format_keyword($clause),
+          $r
+        );
+      }
+    };
+    push @parts, $part;
+  }
+  my $q = $self->join_query_parts(' ', @parts);
+  return $self->join_query_parts('',
+    (our $Render_Top_Level ? $q : ('(', $q, ')'))
+  );
+}
+
 sub _normalize_op {
   my ($self, $raw) = @_;
   my $op = lc $raw;
@@ -1057,6 +1202,17 @@ sub _expand_bind {
   return { -bind => $bind };
 }
 
+sub _expand_values {
+  my ($self, undef, $values) = @_;
+  return { -values => [
+    map +(
+      ref($_) eq 'HASH'
+        ? $self->expand_expr($_)
+        : +{ -row => [ map $self->expand_expr($_), @$_ ] }
+    ), ref($values) eq 'ARRAY' ? @$values : $values
+  ] };
+}
+
 sub _recurse_where {
   my ($self, $where, $logic) = @_;
 
@@ -1209,6 +1365,19 @@ sub _render_op_multop {
   return $self->join_query_parts($join, @parts);
 }
 
+sub _render_values {
+  my ($self, undef, $values) = @_;
+  my $inner = $self->join_query_parts(' ',
+    $self->format_keyword('values'),
+    $self->join_query_parts(', ',
+      ref($values) eq 'ARRAY' ? @$values : $values
+    ),
+  );
+  return $self->join_query_parts('',
+    (our $Render_Top_Level ? $inner : ('(', $inner, ')'))
+  );
+}
+
 sub join_query_parts {
   my ($self, $join, @parts) = @_;
   my @final = map +(