The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package SQL::Abstract::Plugin::InsertMulti;

use strict;
use warnings;

our $VERSION = '0.04';

use Carp ();
use Sub::Exporter -setup => +{
    into    => 'SQL::Abstract',
    exports => [
        qw/insert_multi update_multi _insert_multi _insert_multi_HASHREF _insert_multi_ARRAYREF _insert_multi_values _insert_multi_process_args/
    ],
    groups => +{
        default => [
            qw/insert_multi update_multi _insert_multi _insert_multi_HASHREF _insert_multi_ARRAYREF _insert_multi_values _insert_multi_process_args/
        ]
    },
};

sub insert_multi {
    my $self  = shift;
    my $table = $self->_table(shift);
    my ( $data, $opts, $fields ) = $self->_insert_multi_process_args(@_);
    my ( $sql, @bind ) = $self->_insert_multi( $table, $data, $opts );
    return wantarray ? ( $sql, @bind ) : $sql;
}

sub _insert_multi {
    my ( $self, $table, $data, $opts ) = @_;

    my $method = $self->_METHOD_FOR_refkind( '_insert_multi', $data->[0] );
    my ( $sql, @bind ) = $self->$method( $data, $opts );
    $sql = '( '
      . join( ', ', ( map { $self->_quote($_) } @{ $opts->{fields} } ) ) . ' ) '
      . $sql;

    $sql = join ' ' => grep { defined $_ } (
        $self->_sqlcase('insert'),
        $opts->{option},
        $self->_sqlcase( ( $opts->{ignore} ) ? 'ignore' : 'into' ),
        $table, $sql,
    );

    return ( $sql, @bind );
}

sub _insert_multi_HASHREF {
    my ( $self, $data, $opts ) = @_;
    my ( $sql, @bind ) = $self->_insert_multi_values( $data, $opts );
    return ( $sql, @bind );
}

sub _insert_multi_ARRAYREF {
    my ( $self, $data, $opts ) = @_;
    my ( $sql, @bind ) = $self->_insert_multi_values(
        [
            map {
                my %h;
                @h{ @{ $opts->{fields} } } = @$_;
                \%h;
              } @$data
        ],
        $opts
    );
    return ( $sql, @bind );
}

sub _insert_multi_values {
    my ( $self, $data, $opts ) = @_;

    my ( @value_sqls, @all_bind );

    for my $d (@$data) {
        my @values;
        for my $column ( @{$opts->{fields}} ) {
            my $v = $d->{$column};

            $self->_SWITCH_refkind(
                $v,
                {
                    ARRAYREFREF => sub {    # literal SQL with bind
                        my ( $sql, @bind ) = @${$v};

                        # $self->_assert_bindval_matches_bindtype(@bind);
                        push @values,   $sql;
                        push @all_bind, @bind;
                    },

                    # THINK : anything useful to do with a HASHREF ?
                    HASHREF => sub { # (nothing, but old SQLA passed it through)
                                     #TODO in SQLA >= 2.0 it will die instead
                        push @values, '?';
                        push @all_bind, $self->_bindtype( $column, $v );
                    },
                    SCALARREF => sub {    # literal SQL without bind
                        push @values, $$v;
                    },
                    SCALAR_or_UNDEF => sub {
                        push @values, '?';
                        push @all_bind, $self->_bindtype( $column, $v );
                    },
                }
            );
        }
        push( @value_sqls, '( ' . join( ', ' => @values ) . ' )' );
    }

    my $sql = $self->_sqlcase('values') . ' ' . join( ', ' => @value_sqls );

    if ( $opts->{update} ) {
        my @set;

        for my $k ( sort keys %{ $opts->{update} } ) {
            my $v     = $opts->{update}{$k};
            my $r     = ref $v;
            my $label = $self->_quote($k);

            $self->_SWITCH_refkind(
                $v,
                {
                    ARRAYREFREF => sub {    # literal SQL with bind
                        my ( $sql, @bind ) = @${$v};
                        push @set,      "$label = $sql";
                        push @all_bind, @bind;
                    },
                    SCALARREF => sub {      # literal SQL without bind
                        push @set, "$label = $$v";
                    },
                    SCALAR_or_UNDEF => sub {
                        push @set, "$label = ?";
                        push @all_bind, $self->_bindtype( $k, $v );
                    },
                }
            );
        }

        $sql .=
          $self->_sqlcase(' on duplicate key update ') . join( ', ', @set );
    }

    return ( $sql, @all_bind );
}

sub _insert_multi_process_args {
    my $self = shift;
    my ( $data, $opts, $fields );

    if ( ref $_[0] eq 'ARRAY' && !ref $_[0]->[0] ) {
        $fields = shift;
    }
    else {
        $fields = [ sort keys %{ $_[0]->[0] } ];
    }

    ( $data, $opts ) = @_;

    $opts ||= +{};
    $opts->{fields} ||= $fields;

    return ( $data, $opts );
}

sub update_multi {
    my $self  = shift;
    my $table = $self->_table(shift);
    my ( $data, $opts ) = $self->_insert_multi_process_args(@_);

    my %ignore;
    if ($opts->{update_ignore_fields}) {
        @ignore{@{$opts->{update_ignore_fields}}} = map { 1 } @{$opts->{update_ignore_fields}};
    }
    
    $opts->{update} = +{
        map {
            my ( $k, $v ) = ( $_, $self->_sqlcase('values( ') . $_ . ' )' );
            ( $k, \$v );
        }
        grep { !exists $ignore{$_} }
        @{ $opts->{fields} }
    };

    my ( $sql, @bind ) = $self->_insert_multi( $table, $data, $opts );
    return wantarray ? ( $sql, @bind ) : $sql;
}

1;
__END__

=head1 NAME

SQL::Abstract::Plugin::InsertMulti - add mysql bulk insert supports for SQL::Abstract

=head1 SYNOPSIS

  use SQL::Abstract;
  use SQL::Abstract::Plugin::InsertMulti;

  my $sql = SQL::Abstract->new;
  my ($stmt, @bind) = $sql->insert_multi('people', [
    +{ name => 'foo', age => 23, },
    +{ name => 'bar', age => 40, },
  ]);

=head1 DESCRIPTION

SQL::Abstract::Plugin::InsertMulti is enable bulk insert support for L<SQL::Abstract>. Declare 'use SQL::Abstract::Plugin::InsertMulti;' with 'use SQL::Abstract;',
exporting insert_multi() and update_multi() methods to L<SQL::Abstract> namespace from SQL::Abstract::Plugin::InsertMulti.
Plugin system is depends on 'into' options of L<Sub::Exporter>.

Notice: please check your mysql_allow_packet parameter using this module.

=head1 METHODS

=head2 insert_multi($table, \@data, \%opts)

  my ($stmt, @bind) = $sql->insert_multi('foo', [ +{ a => 1, b => 2, c => 3 }, +{ a => 4, b => 5, c => 6, }, ]);
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? )|
  # @bind = (1, 2, 3, 4, 5, 6);

@data is HashRef list.
%opts details is below.

=over

=item ignore

Use 'INSERT IGNORE' instead of 'INSERT INTO'.

=item update

Use 'ON DUPLICATE KEY UPDATE'.
This value is same as update()'s data parameters.

=item update_ignore_fields

update_multi() method is auto generating 'ON DUPLICATE KEY UPDATE' parameters:

  my ($stmt, @bind) = $sql->update_multi('foo', [qw/a b c/], [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]);
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? ) ON DUPLICATE KEY UPDATE a = VALUES( a ), b = VALUES( b ), c = VALUES( c )|
  # @bind = (1, 2, 3, 4, 5, 6);

given update_ignore_fields,

  my ($stmt, @bind) = $sql->update_multi('foo', [qw/a b c/], [ [ 1, 2, 3 ], [ 4, 5, 6 ] ], +{ update_ignore_fields => [qw/b c/], });
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? ) ON DUPLICATE KEY UPDATE a = VALUES( a )|
  # @bind = (1, 2, 3, 4, 5, 6);

=back

=head2 insert_multi($table, \@field, \@data, \%opts)

  my ($stmt, @bind) = $sql->insert_multi('foo', [qw/a b c/], [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]);
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? )|
  # @bind = (1, 2, 3, 4, 5, 6);

@data is ArrayRef list. See also L<insert_multi($table, \@data, \%opts)> %opts details.

=head2 update_multi($table, \@data, \%opts)

@data is HashRef list. See also L<insert_multi($table, \@data, \%opts)> %opts details.

  my ($stmt, @bind) = $sql->update_multi('foo', [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]);
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? ) ON DUPLICATE KEY UPDATE a = VALUES( a ), b = VALUES( b ), c = VALUES( c )|
  # @bind = (1, 2, 3, 4, 5, 6);

=head2 update_multi($table, \@field, \@data, \%opts)

  my ($stmt, @bind) = $sql->update_multi('foo', [qw/a b c/], [ +{ a => 1, b => 2, c => 3 }, +{ a => 4, b => 5, c => 6, }, ]);
  # $stmt = q|INSERT INTO foo( a, b, c ) VALUES ( ?, ?, ? ), ( ?, ?, ? ) ON DUPLICATE KEY UPDATE a = VALUES( a ), b = VALUES( b ), c = VALUES( c )|
  # @bind = (1, 2, 3, 4, 5, 6);

@data is ArrayRef list. See also L<insert_multi($table, \@data, \%opts)> %opts details.

=head1 AUTHOR

Toru Yamaguchi E<lt>zigorou@cpan.orgE<gt>

Thanks ma.la L<http://subtech.g.hatena.ne.jp/mala/>. This module is based on his source codes.

=head1 SEE ALSO

=over

=item http://subtech.g.hatena.ne.jp/mala/20090729/1248880239

=item http://gist.github.com/158203

=item L<SQL::Abstract>

=item L<Sub::Exporter>

=back

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut