Skip to content

Commit

Permalink
feat: also add populate_upsert
Browse files Browse the repository at this point in the history
  • Loading branch information
rabbiveesh committed Jun 16, 2024
1 parent d03c83d commit bef07dc
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 16 deletions.
53 changes: 37 additions & 16 deletions lib/DBIx/Class/ResultSet/SQLA2Support.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,38 @@ use warnings;
use experimental 'signatures';
use parent 'DBIx::Class::ResultSet';
use List::Util 'pairmap';
use Carp 'croak';

my sub _create_upsert_clause ($self, $columns_ary, $overrides) {
my %pks = map +($_ => 1), $self->result_source->primary_columns;
return +{
-target => [ keys %pks ],
-set => {
# create a hash of non-pk cols w/ the value excluded.$_, which does the upsert
(map +($_ => { -ident => "excluded.$_" }), grep !$pks{$_}, $columns_ary->@*),
# and allow overrides from the caller
$overrides->%*
}
};
}

sub upsert ($self, $to_insert, $overrides = {}) {
# in case there are other passthroughs, you never know
my $sqla2_passthru = delete $to_insert->{-sqla2} || {};

# generate our on_conflict clause
my $to_upsert = { $to_insert->%* };
my $source = $self->result_source;
my @pks = $source->primary_columns;
delete @$to_upsert{@pks};

# evil handling for RETURNING, b/c DBIC doesn't give us a place to do it properly.
# Basically force each input value to be a ref, and update the column config to use
# RETURNING, thus ensuring we get RETURNING handling
for my $col (keys $overrides->%*) {
next if ref $to_insert->{$col};
$to_insert->{$col} = \[ '?' => $to_insert->{$col} ];
}
my $source = $self->result_source;
local $source->{_columns}
= { pairmap { $a => { $b->%*, exists $overrides->{$a} ? (retrieve_on_insert => 1) : () } }
$source->{_columns}->%* };

$sqla2_passthru->{on_conflict} = {
-target => \@pks,
-set => {
# unroll all upserty columns
(map +($_ => { -ident => "excluded.$_" }), keys $to_upsert->%*),
# and allow overrides from the caller
$overrides->%*
}
};
$sqla2_passthru->{on_conflict} = _create_upsert_clause($self, [ keys $to_insert->%* ], $overrides);
$self->create({ $to_insert->%*, -sqla2 => $sqla2_passthru });
}

Expand All @@ -45,6 +46,17 @@ sub populate ($self, $to_insert, $attrs = undef) {
$self->next::method($to_insert);
}

sub populate_upsert ($self, $to_insert, $overrides = {}) {
croak "populate_upsert must be called in void context" if defined wantarray;
my @inserted_cols;
if (ref $to_insert->[0] eq 'ARRAY') {
@inserted_cols = $to_insert->[0]->@*;
} else {
@inserted_cols = keys $to_insert->[0]->%*;
}
$self->populate($to_insert, { on_conflict => _create_upsert_clause($self, \@inserted_cols, $overrides) });
}

1;

=encoding utf8
Expand Down Expand Up @@ -75,7 +87,9 @@ addition, provides some extra methods.
=head2 METHODS
=over 2 upsert
=over 2
=item upsert
# on conflict, add this name to the existing name
$rs->upsert({ name => 'thingy', id => 9001 }, { name => \"name || ' ' || excluded.name" });
Expand All @@ -86,6 +100,13 @@ as the second argument to override the default on conflict value. You can pass i
(literal SQL is quite helpful here) and it will be retrieved by DBIC on the insert using
DBIC's return_on_insert functionality.
=item populate_upsert
# on conflict, add this name to the existing name
$rs->populate_upsert([{ name => 'thingy', id => 9001 }, { name => 'over 9000', id => 9002 }], { name => \"name || ' ' || excluded.name" });
Same as C<upsert> above, just for C<populate>. Dies a horrible death if called in non-void context.
=back
=cut
13 changes: 13 additions & 0 deletions t/upsert.t
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,17 @@ subtest 'update' => sub {
is $schema->resultset('Artist')->find(3)->{name}, 'LSB', '-upsert is a shortcut!';
};

subtest 'populate_upsert' => sub {
my $artist_rs = $schema->resultset('Artist');
$artist_rs->populate([ { artistid => 9000, name => 'Fame' }, { artistid => 9001, name => 'Lame' }]);

$artist_rs->populate_upsert([ { artistid => 9000, name => 'Fame' }, { artistid => 9001, name => 'Lame' }], { name => \"name || '--' || excluded.name" });

my @artists = $artist_rs->search({ artistid => { '>=' => 9000 }}, { order_by => 'artistid' });

is $artists[0]->{name}, 'Fame--Fame', 'first artist updated';
is $artists[1]->{name}, 'Lame--Lame', 'second artist updated';

};

done_testing;

0 comments on commit bef07dc

Please sign in to comment.