Skip to content

Commit d0cefd9

Browse files
committed
Entirely and utterly rewrite populate(), fixing the variable hash issue
Yes, it took ~3 years to properly fix it. The changeset size alone should make it pretty clear why this happened, but this is not the entire story. At first the bug was deemed fixed back in a9bac98. Due to miscommunication and lack of tests this issue did not come up again until last winter (the devs thought they nailed it, the users thought it's a wontfix). Then when the actual tests got written it became clear that... words fail me. In short - some folks had the bright idea that a fast-path insert got to be able to understand *MULTI-CREATE* semantics. There was a great deal of tests that did all the wrong things from a conceptual point of view but they were passing tests nonetheless. So the thing got tabled again... In the meantime a recent flurry of improvements to the relcond resolver took place, mainly centered around fixing the custom relconds call modes. A side effect was uncovering that populate() is invoking relationship cond resolution in an... insane for the lack of a better word way. Some shims were put in place and the only remaining bit were warnings, however with the improvements available it in fact possible to cleanly implement "subquery-based multicreate". So instead of punting - the entire codeflow of populate was reworked with the new toys in mind. The data mangler _normalize_populate_args is gone for good (no more mindless copy of passed in data). The amount of fallbacks to create() is reduced and each instance now properly warns of the problem. All in all this is another one of "if something changed - I fucked up" changes. As an added benefit - we now have a foothold into validating subquery-based multicreate implementations - soon (one would hope) the code will migrate to wider use within ::ResultSet. A notable part of this commit is the *undocumented* implementing part of the Swindon consensus - the is_depends_on flag is now added to all belongs_to relationship attrs. The actual implementation of the rest is subject to another battle. Now on to 0.082800
1 parent c19ca6e commit d0cefd9

File tree

6 files changed

+337
-100
lines changed

6 files changed

+337
-100
lines changed

Changes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Revision history for DBIx::Class
2525
resultsets with no rows
2626
- Fix incorrect handling of custom relationship conditions returning
2727
SQLA literal expressions
28+
- Fix long standing bug with populate() getting confused by hashrefs
29+
with different sets of keys: http://is.gd/2011_dbic_populate_gotcha
2830
- Fix multi-value literal populate not working with simplified bind
2931
specifications
3032
- Massively improve the implied resultset condition parsing - now all

lib/DBIx/Class/Relationship/BelongsTo.pm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ sub belongs_to {
8989
$class->add_relationship($rel, $f_class,
9090
$cond,
9191
{
92+
is_depends_on => 1,
9293
accessor => $acc_type,
9394
$fk_columns ? ( fk_columns => $fk_columns ) : (),
9495
%{$attrs || {}}

lib/DBIx/Class/ResultSet.pm

Lines changed: 225 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,127 +2234,266 @@ case there are obviously no benefits to using this method over L</create>.
22342234
sub populate {
22352235
my $self = shift;
22362236

2237-
# cruft placed in standalone method
2238-
my $data = $self->_normalize_populate_args(@_);
2237+
my ($data, $guard);
22392238

2240-
return unless @$data;
2239+
# this is naive and just a quick check
2240+
# the types will need to be checked more thoroughly when the
2241+
# multi-source populate gets added
2242+
if (ref $_[0] eq 'ARRAY') {
2243+
return unless @{$_[0]};
22412244

2242-
if(defined wantarray) {
2243-
my @created = map { $self->new_result($_)->insert } @$data;
2244-
return wantarray ? @created : \@created;
2245+
$data = $_[0] if (ref $_[0][0] eq 'HASH' or ref $_[0][0] eq 'ARRAY');
22452246
}
2246-
else {
2247-
my $first = $data->[0];
22482247

2249-
# if a column is a registered relationship, and is a non-blessed hash/array, consider
2250-
# it relationship data
2251-
my (@rels, @columns);
2252-
my $rsrc = $self->result_source;
2253-
my $rels = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
2254-
for (keys %$first) {
2255-
my $ref = ref $first->{$_};
2256-
$rels->{$_} && ($ref eq 'ARRAY' or $ref eq 'HASH')
2257-
? push @rels, $_
2258-
: push @columns, $_
2248+
$self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs')
2249+
unless $data;
2250+
2251+
# FIXME - no cref handling
2252+
# At this point assume either hashes or arrays
2253+
2254+
if(defined wantarray) {
2255+
my @results;
2256+
2257+
$guard = $self->result_source->schema->storage->txn_scope_guard
2258+
if ( @$data > 2 or ( @$data == 2 and ref $data->[0] eq 'ARRAY' ) );
2259+
2260+
if (ref $data->[0] eq 'ARRAY') {
2261+
@results = map
2262+
{ my $vals = $_; $self->new_result({ map { $data->[0][$_] => $vals->[$_] } 0..$#{$data->[0]} })->insert }
2263+
@{$data}[1 .. $#$data]
22592264
;
22602265
}
2266+
else {
2267+
@results = map { $self->new_result($_)->insert } @$data;
2268+
}
2269+
2270+
$guard->commit if $guard;
2271+
return wantarray ? @results : \@results;
2272+
}
2273+
2274+
# we have to deal with *possibly incomplete* related data
2275+
# this means we have to walk the data structure twice
2276+
# whether we want this or not
2277+
# jnap, I hate you ;)
2278+
my $rsrc = $self->result_source;
2279+
my $rel_info = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
2280+
2281+
my ($colinfo, $colnames, $slices_with_rels);
2282+
my $data_start = 0;
2283+
2284+
DATA_SLICE:
2285+
for my $i (0 .. $#$data) {
2286+
2287+
my $current_slice_seen_rel_infos;
22612288

2262-
my @pks = $rsrc->primary_columns;
2289+
### Determine/Supplement collists
2290+
### BEWARE - This is a hot piece of code, a lot of weird idioms were used
2291+
if( ref $data->[$i] eq 'ARRAY' ) {
22632292

2264-
## do the belongs_to relationships
2265-
foreach my $index (0..$#$data) {
2293+
# positional(!) explicit column list
2294+
if ($i == 0) {
22662295

2267-
# delegate to create() for any dataset without primary keys with specified relationships
2268-
if (grep { !defined $data->[$index]->{$_} } @pks ) {
2269-
for my $r (@rels) {
2270-
if (grep { ref $data->[$index]{$r} eq $_ } qw/HASH ARRAY/) { # a related set must be a HASH or AoH
2271-
my @ret = $self->populate($data);
2272-
return;
2296+
$colinfo->{$data->[0][$_]} = { pos => $_, name => $data->[0][$_] } and push @$colnames, $data->[0][$_]
2297+
for 0 .. $#{$data->[0]};
2298+
2299+
$data_start = 1;
2300+
2301+
next DATA_SLICE;
2302+
}
2303+
else {
2304+
for (values %$colinfo) {
2305+
if ($_->{is_rel} ||= (
2306+
$rel_info->{$_->{name}}
2307+
and
2308+
(
2309+
ref $data->[$i][$_->{pos}] eq 'ARRAY'
2310+
or
2311+
ref $data->[$i][$_->{pos}] eq 'HASH'
2312+
or
2313+
( defined blessed $data->[$i][$_->{pos}] and $data->[$i][$_->{pos}]->isa('DBIx::Class::Row') )
2314+
)
2315+
and
2316+
1
2317+
)) {
2318+
2319+
# moar sanity check... sigh
2320+
for ( ref $data->[$i][$_->{pos}] eq 'ARRAY' ? @{$data->[$i][$_->{pos}]} : $data->[$i][$_->{pos}] ) {
2321+
if ( defined blessed $_ and $_->isa('DBIx::Class::Row' ) ) {
2322+
carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
2323+
return my $throwaway = $self->populate(@_);
2324+
}
2325+
}
2326+
2327+
push @$current_slice_seen_rel_infos, $rel_info->{$_->{name}};
22732328
}
22742329
}
22752330
}
22762331

2277-
foreach my $rel (@rels) {
2278-
next unless ref $data->[$index]->{$rel} eq "HASH";
2279-
my $result = $self->related_resultset($rel)->new_result($data->[$index]->{$rel})->insert;
2280-
my (undef, $reverse_relinfo) = %{$rsrc->reverse_relationship_info($rel)};
2281-
my $related = $result->result_source->_resolve_condition(
2282-
$reverse_relinfo->{cond},
2283-
$self,
2284-
$result,
2285-
$rel,
2286-
);
2287-
2288-
delete $data->[$index]->{$rel};
2289-
$data->[$index] = {%{$data->[$index]}, %$related};
2290-
2291-
push @columns, keys %$related if $index == 0;
2332+
if ($current_slice_seen_rel_infos) {
2333+
push @$slices_with_rels, { map { $colnames->[$_] => $data->[$i][$_] } 0 .. $#$colnames };
2334+
2335+
# this is needed further down to decide whether or not to fallback to create()
2336+
$colinfo->{$colnames->[$_]}{seen_null} ||= ! defined $data->[$i][$_]
2337+
for 0 .. $#$colnames;
22922338
}
22932339
}
2340+
elsif( ref $data->[$i] eq 'HASH' ) {
22942341

2295-
## inherit the data locked in the conditions of the resultset
2296-
my ($rs_data) = $self->_merge_with_rscond({});
2297-
delete @{$rs_data}{@columns};
2298-
2299-
## do bulk insert on current row
2300-
$rsrc->storage->insert_bulk(
2301-
$rsrc,
2302-
[@columns, keys %$rs_data],
2303-
[ map { [ @$_{@columns}, values %$rs_data ] } @$data ],
2304-
);
2342+
for ( sort keys %{$data->[$i]} ) {
23052343

2306-
## do the has_many relationships
2307-
foreach my $item (@$data) {
2344+
$colinfo->{$_} ||= do {
23082345

2309-
my $main_row;
2346+
$self->throw_exception("Column '$_' must be present in supplied explicit column list")
2347+
if $data_start; # it will be 0 on AoH, 1 on AoA
23102348

2311-
foreach my $rel (@rels) {
2312-
next unless ref $item->{$rel} eq "ARRAY" && @{ $item->{$rel} };
2349+
push @$colnames, $_;
23132350

2314-
$main_row ||= $self->new_result({map { $_ => $item->{$_} } @pks});
2351+
# RV
2352+
{ pos => $#$colnames, name => $_ }
2353+
};
23152354

2316-
my $child = $main_row->$rel;
2355+
if ($colinfo->{$_}{is_rel} ||= (
2356+
$rel_info->{$_}
2357+
and
2358+
(
2359+
ref $data->[$i]{$_} eq 'ARRAY'
2360+
or
2361+
ref $data->[$i]{$_} eq 'HASH'
2362+
or
2363+
( defined blessed $data->[$i]{$_} and $data->[$i]{$_}->isa('DBIx::Class::Row') )
2364+
)
2365+
and
2366+
1
2367+
)) {
2368+
2369+
# moar sanity check... sigh
2370+
for ( ref $data->[$i]{$_} eq 'ARRAY' ? @{$data->[$i]{$_}} : $data->[$i]{$_} ) {
2371+
if ( defined blessed $_ and $_->isa('DBIx::Class::Row' ) ) {
2372+
carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
2373+
return my $throwaway = $self->populate(@_);
2374+
}
2375+
}
23172376

2318-
my $related = $child->result_source->_resolve_condition(
2319-
$rels->{$rel}{cond},
2320-
$child,
2321-
$main_row,
2322-
$rel,
2323-
);
2377+
push @$current_slice_seen_rel_infos, $rel_info->{$_};
2378+
}
2379+
}
23242380

2325-
my @rows_to_add = ref $item->{$rel} eq 'ARRAY' ? @{$item->{$rel}} : ($item->{$rel});
2326-
my @populate = map { {%$_, %$related} } @rows_to_add;
2381+
if ($current_slice_seen_rel_infos) {
2382+
push @$slices_with_rels, $data->[$i];
23272383

2328-
$child->populate( \@populate );
2384+
# this is needed further down to decide whether or not to fallback to create()
2385+
$colinfo->{$_}{seen_null} ||= ! defined $data->[$i]{$_}
2386+
for keys %{$data->[$i]};
23292387
}
23302388
}
2389+
else {
2390+
$self->throw_exception('Unexpected populate() data structure member type: ' . ref $data->[$i] );
2391+
}
2392+
2393+
if ( grep
2394+
{ $_->{attrs}{is_depends_on} }
2395+
@{ $current_slice_seen_rel_infos || [] }
2396+
) {
2397+
carp_unique("Fast-path populate() of belongs_to relationship data is not possible - falling back to regular create()");
2398+
return my $throwaway = $self->populate(@_);
2399+
}
23312400
}
2332-
}
23332401

2402+
if( $slices_with_rels ) {
23342403

2335-
# populate() arguments went over several incarnations
2336-
# What we ultimately support is AoH
2337-
sub _normalize_populate_args {
2338-
my ($self, $arg) = @_;
2404+
# need to exclude the rel "columns"
2405+
$colnames = [ grep { ! $colinfo->{$_}{is_rel} } @$colnames ];
23392406

2340-
if (ref $arg eq 'ARRAY') {
2341-
if (!@$arg) {
2342-
return [];
2343-
}
2344-
elsif (ref $arg->[0] eq 'HASH') {
2345-
return $arg;
2407+
# extra sanity check - ensure the main source is in fact identifiable
2408+
# the localizing of nullability is insane, but oh well... the use-case is legit
2409+
my $ci = $rsrc->columns_info($colnames);
2410+
2411+
$ci->{$_} = { %{$ci->{$_}}, is_nullable => 0 }
2412+
for grep { ! $colinfo->{$_}{seen_null} } keys %$ci;
2413+
2414+
unless( $rsrc->_identifying_column_set($ci) ) {
2415+
carp_unique("Fast-path populate() of non-uniquely identifiable rows with related data is not possible - falling back to regular create()");
2416+
return my $throwaway = $self->populate(@_);
23462417
}
2347-
elsif (ref $arg->[0] eq 'ARRAY') {
2348-
my @ret;
2349-
my @colnames = @{$arg->[0]};
2350-
foreach my $values (@{$arg}[1 .. $#$arg]) {
2351-
push @ret, { map { $colnames[$_] => $values->[$_] } (0 .. $#colnames) };
2418+
}
2419+
2420+
### inherit the data locked in the conditions of the resultset
2421+
my ($rs_data) = $self->_merge_with_rscond({});
2422+
delete @{$rs_data}{@$colnames}; # passed-in stuff takes precedence
2423+
2424+
# if anything left - decompose rs_data
2425+
my $rs_data_vals;
2426+
if (keys %$rs_data) {
2427+
push @$rs_data_vals, $rs_data->{$_}
2428+
for sort keys %$rs_data;
2429+
}
2430+
2431+
### start work
2432+
$guard = $rsrc->schema->storage->txn_scope_guard
2433+
if $slices_with_rels;
2434+
2435+
### main source data
2436+
# FIXME - need to switch entirely to a coderef-based thing,
2437+
# so that large sets aren't copied several times... I think
2438+
$rsrc->storage->insert_bulk(
2439+
$rsrc,
2440+
[ @$colnames, sort keys %$rs_data ],
2441+
[ map {
2442+
ref $data->[$_] eq 'ARRAY'
2443+
? (
2444+
$slices_with_rels ? [ @{$data->[$_]}[0..$#$colnames], @{$rs_data_vals||[]} ] # the collist changed
2445+
: $rs_data_vals ? [ @{$data->[$_]}, @$rs_data_vals ]
2446+
: $data->[$_]
2447+
)
2448+
: [ @{$data->[$_]}{@$colnames}, @{$rs_data_vals||[]} ]
2449+
} $data_start .. $#$data ],
2450+
);
2451+
2452+
### do the children relationships
2453+
if ( $slices_with_rels ) {
2454+
my @rels = grep { $colinfo->{$_}{is_rel} } keys %$colinfo
2455+
or die 'wtf... please report a bug with DBIC_TRACE=1 output (stacktrace)';
2456+
2457+
for my $sl (@$slices_with_rels) {
2458+
2459+
my ($main_proto, $main_proto_rs);
2460+
for my $rel (@rels) {
2461+
next unless defined $sl->{$rel};
2462+
2463+
$main_proto ||= {
2464+
%$rs_data,
2465+
(map { $_ => $sl->{$_} } @$colnames),
2466+
};
2467+
2468+
unless (defined $colinfo->{$rel}{rs}) {
2469+
2470+
$colinfo->{$rel}{rs} = $rsrc->related_source($rel)->resultset;
2471+
2472+
$colinfo->{$rel}{fk_map} = { reverse %{ $rsrc->_resolve_relationship_condition(
2473+
rel_name => $rel,
2474+
self_alias => "\xFE", # irrelevant
2475+
foreign_alias => "\xFF", # irrelevant
2476+
)->{identity_map} || {} } };
2477+
2478+
}
2479+
2480+
$colinfo->{$rel}{rs}->search({ map # only so that we inherit them values properly, no actual search
2481+
{
2482+
$_ => { '=' =>
2483+
( $main_proto_rs ||= $rsrc->resultset->search($main_proto) )
2484+
->get_column( $colinfo->{$rel}{fk_map}{$_} )
2485+
->as_query
2486+
}
2487+
}
2488+
keys %{$colinfo->{$rel}{fk_map}}
2489+
})->populate( ref $sl->{$rel} eq 'ARRAY' ? $sl->{$rel} : [ $sl->{$rel} ] );
2490+
2491+
1;
23522492
}
2353-
return \@ret;
23542493
}
23552494
}
23562495

2357-
$self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs');
2496+
$guard->commit if $guard;
23582497
}
23592498

23602499
=head2 pager

0 commit comments

Comments
 (0)