Hi Peter,

The problem goes a little deeper:  I applied this change to SVN, and
immediately saw the 60core.t unit test fail doing deep recursion.
Apparently this will break when there's a loop in the relationship
graph.  So, to avoid the loop, I made a version of delete_all which
has loop protection.  That fixes my unit test, but it breaks again in
60core.t because one of the search_related()->delete() calls produces
ambigious SQL.  Sigh.

I attached my patch, which would reproduce this error when aplied to SVN:

make test
...
ok 15 - Correct artist too
DBIx::Class::Relationship::CascadeActions::delete(): DBI Exception:
DBD::SQLite::db prepare_cached failed: ambiguous column name: id2(1)
at dbdimp.c line 271 [for Statement "SELECT
artist_undirected_maps.id1, artist_undirected_maps.id2 FROM
artist_undirected_map me LEFT JOIN artist mapped_artists ON (
mapped_artists.artistid = me.id1 ) OR ( mapped_artists.artistid =
me.id2 ) LEFT JOIN artist_undirected_map artist_undirected_maps ON (
artist_undirected_maps.id1 = mapped_artists.artistid ) OR (
artist_undirected_maps.id2 = mapped_artists.artistid ) WHERE ( ( id1 =
? ) OR ( id2 = ? ) )"] at t/60core.t line 74


I'm not sure how to fix the ambiguous column name error though.  Any thoughts?

~Noel



On Fri, Oct 24, 2008 at 2:32 PM, Peter Rabbitson <[EMAIL PROTECTED]> wrote:
> Noel Burton-Krahn wrote:
>> DBIx's cascading delete_all (in DBIx::Class::ResultSet) it broken,
>> because it deletes the parent table before it deletes the children.
>> The database will throw a referential integrity exception when the
>> parent is deleted before the children.    I've attached a test program
>> below.  Here's a fixed version in
>> DBIx-Class-0.08010/lib/DBIx/Class/Relationship/CascadeActions.pm:
>>
>> Regards,
>> Noel Burton-Krahn
>>
>> ###################################
>> # fixed DBIx/Class/Relationship/CascadeActions.pm in DBIx-Class-0.08010
>>
>> sub delete {
>>   my ($self, @rest) = @_;
>>
>>   # delete from tables that depend on me first
>>   my $source = $self->result_source;
>>   my %rels = map { $_ => $source->relationship_info($_) }
>> $source->relationships;
>>   my @cascade = grep { $rels{$_}{attrs}{cascade_delete} } keys %rels;
>>   foreach my $rel (@cascade) {
>>     $self->search_related($rel)->delete_all;
>>   }
>>
>>   # delete me
>>   return $self->next::method(@rest) unless ref $self;
>>     # I'm just ignoring this for class deletes because hell, the db should
>>     # be handling this anyway. Assuming we have joins we probably actually
>>     # *could* do them, but I'd rather not.
>>
>>   my $ret = $self->next::method(@rest);
>>
>>   return $ret;
>> }
>>
>>
>>
>> # Test program
>> ###################################
>> #! /usr/bin/perl -w
>> =head1 NAME
>>
>>  dbix_cascade_delete.t - reproduce DBIx's failure in delete_all
>>
>> =head1 DESCRIPTION
>>
>> DBIx::Class::ResultSet::delete_all fails in version 0.08010 because it
>> deletes the parent before the children
>>
>> =head1 AUTHOR
>>
>> Noel Burton-Krahn <[EMAIL PROTECTED]>
>>
>> =cut
>>
>> use strict;
>> use warnings;
>>
>> #--------------------
>> package My::DBIx::Class;
>> use base qw/DBIx::Class/;
>>
>> __PACKAGE__->load_components(qw/PK::Auto Core/);
>>
>> use overload '""' => 'dump';
>>
>> sub dump {
>>       my($self) = shift;
>>       return join(" ", map { "$_=" . $self->get_column($_) } $self->columns);
>> }
>>
>> #--------------------
>> package MySchema::Person;
>> use base qw/My::DBIx::Class/;
>> __PACKAGE__->table('person');
>> __PACKAGE__->add_columns(qw(person_id name));
>> __PACKAGE__->set_primary_key('person_id');
>> __PACKAGE__->has_many(address => 'MySchema::Address', 'person_id');
>>
>> #--------------------
>> package MySchema::Address;
>> use base  qw/My::DBIx::Class/;
>> __PACKAGE__->table('address');
>> __PACKAGE__->add_columns(qw(address_id person_id address));
>> __PACKAGE__->set_primary_key('address_id');
>> __PACKAGE__->belongs_to(person => 'MySchema::Person', 'person_id');
>>
>> #--------------------
>> package MySchema;
>> use base qw/DBIx::Class::Schema/;
>>
>> __PACKAGE__->load_classes({
>>       'MySchema' => [ qw(Person Address) ],
>> });
>>
>> #--------------------
>> package Test::DbixCascaseDelete;
>> use Test::More tests => 16;
>>
>> # create a mysql database to test with
>> system(<<'EOS');
>> mysqladmin -f drop mytest >/dev/null 2>&1
>>
>> mysqladmin create mytest
>>
>> mysql mytest <<ESQL
>> create table person (
>>   person_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
>>   ,name VARCHAR(1024) NOT NULL
>> ) ENGINE=INNODB;
>>
>> create table address (
>>   address_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
>>   ,person_id INT NOT NULL
>>   ,address VARCHAR(1024) NOT NULL
>>   ,FOREIGN KEY (person_id) REFERENCES person (person_id)
>> ) ENGINE=INNODB;
>> ESQL
>>
>> #mysql mytest <<ESQL
>> #show tables;
>> #show create table person;
>> #show create table address;
>> #ESQL
>>
>> EOS
>>       ;
>> is($?, 0, "create database");
>>
>> # connect
>> my $schema = MySchema->connect("dbi:mysql:mytest", 'script', 'tlby14')
>> or die("connect: $!");
>> ok($schema, "connect to db");
>>
>> #$schema->storage->debug(1);
>>
>> my $rs;
>> my $person;
>>
>> $person = $schema->resultset('Person')->create({ name => 'fred'});
>> ok($person, "create Person: $person");
>>
>> $rs = $schema->resultset('Person')->search();
>> while( my $row = $rs->next() ) {
>>       $person = $row;
>> }
>> ok($rs, "found Person: $person");
>>
>> my $address;
>> for my $i (1..3) {
>>       $address = $schema->resultset('Address')->create({ person => $person,
>> address => "fred's address $i"});
>>       ok($address, "create Address: $address");
>> }
>>
>> $rs = $schema->resultset('Address')->search({ person_id =>
>> $person->person_id });
>> while( my $row = $rs->next ) {
>>       $address = $row;
>>       ok($address, "found created Address: $address");
>> }
>>
>> ok($address->person, "address->person: " . $address->person->dump);
>>
>> $rs = $person->address_rs;
>> while( my $row = $rs->next ) {
>>       $address = $row;
>>       ok($address, "person->address: $address");
>> }
>>
>> $rs = $schema->resultset('Person')->search({ name => 'fred'});
>>
>> $rs->delete_all;
>> ok(1, "delete_all");
>>
>> is($rs->count, 0, "Person really gone");
>>
>
> Please submit the lonely pieces of code above as a real diff against
> dbic trunk[1], so the actual set of changes is clearly visible
> (facilitates review and potential inclusion).
>
> Cheers
>
> [1]: svn co http://dev.catalyst.perl.org/repos/bast/DBIx-Class/0.08/trunk
>
> _______________________________________________
> List: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class
> IRC: irc.perl.org#dbix-class
> SVN: http://dev.catalyst.perl.org/repos/bast/DBIx-Class/
> Searchable Archive: http://www.grokbase.com/group/dbix-class@lists.scsys.co.uk
>
Index: t/100delete_all_cascades.t
===================================================================
--- t/100delete_all_cascades.t	(revision 0)
+++ t/100delete_all_cascades.t	(revision 0)
@@ -0,0 +1,136 @@
+#! /usr/bin/perl -w
+=head1 NAME
+
+ dbix_cascade_delete.t - reproduce DBIx's failure in delete_all
+
+=head1 DESCRIPTION 
+
+DBIx::Class::ResultSet::delete_all fails in version 0.08010 because it
+deletes the parent before the children
+
+=head1 AUTHOR
+
+Noel Burton-Krahn <[EMAIL PROTECTED]>
+
+=cut
+
+use strict;
+use warnings;
+
+#--------------------
+package My::DBIx::Class;
+use base qw/DBIx::Class/;
+
+__PACKAGE__->load_components(qw/PK::Auto Core/);
+
+use overload '""' => 'dump';
+
+sub dump {
+	my($self) = shift;
+	return join(" ", map { "$_=" . $self->get_column($_) } $self->columns);
+}
+
+#--------------------
+package MySchema::Person;
+use base qw/My::DBIx::Class/;
+__PACKAGE__->table('person');
+__PACKAGE__->add_columns(qw(id name));
+__PACKAGE__->set_primary_key('id');
+__PACKAGE__->has_many(address => 'MySchema::Address', 'person_id');
+
+#--------------------
+package MySchema::Address;
+use base  qw/My::DBIx::Class/;
+__PACKAGE__->table('address');
+__PACKAGE__->add_columns(qw(id person_id address));
+__PACKAGE__->set_primary_key('id');
+__PACKAGE__->belongs_to(person => 'MySchema::Person', 'person_id');
+
+#--------------------
+package MySchema;
+use base qw/DBIx::Class::Schema/;
+
+__PACKAGE__->load_classes({
+	'MySchema' => [ qw(Person Address) ],
+});
+
+#--------------------
+package Test::DbixCascaseDelete;
+use Test::More tests => 16;
+
+# create a mysql database to test with
+system(<<'EOS');
+mysqladmin -f drop mytest >/dev/null 2>&1
+
+mysqladmin create mytest
+
+mysql mytest <<ESQL
+create table person (
+  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
+  ,name VARCHAR(1024) NOT NULL
+) ENGINE=INNODB;
+
+create table address (
+  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
+  ,person_id INT NOT NULL
+  ,address VARCHAR(1024) NOT NULL
+  ,FOREIGN KEY (person_id) REFERENCES person (id)
+) ENGINE=INNODB;
+ESQL
+
+#mysql mytest <<ESQL
+#show tables;
+#show create table person;
+#show create table address;
+#ESQL
+
+EOS
+	;
+is($?, 0, "create database");
+
+# connect
+my $schema = MySchema->connect("dbi:mysql:mytest", 'script', 'tlby14') or die("connect: $!");
+ok($schema, "connect to db");
+
+#$schema->storage->debug(1);
+
+my $rs;
+my $person;
+
+$person = $schema->resultset('Person')->create({ name => 'fred'});
+ok($person, "create Person: $person");
+
+$rs = $schema->resultset('Person')->search();
+while( my $row = $rs->next() ) {
+	$person = $row;
+}
+ok($rs, "found Person: $person");
+
+my $address;
+for my $i (1..3) {
+	$address = $schema->resultset('Address')->create({ person => $person, address => "fred's address $i"});
+	ok($address, "create Address: $address");
+}
+
+$rs = $schema->resultset('Address');
+while( my $row = $rs->next ) {
+	$address = $row;
+	ok($address, "found created Address: $address");
+}
+
+ok($address->person, "address->person: " . $address->person->dump);
+
+$rs = $person->search_related('address');
+while( my $row = $rs->next ) {
+	$address = $row;
+	ok($address, "person->address: $address");
+}
+
+$rs = $schema->resultset('Person')->search({ name => 'fred'});
+
+$rs->delete_all;
+ok(1, "delete_all");
+
+is($rs->count, 0, "Person really gone");
+
+
Index: lib/DBIx/Class/ResultSet.pm
===================================================================
--- lib/DBIx/Class/ResultSet.pm	(revision 4975)
+++ lib/DBIx/Class/ResultSet.pm	(working copy)
@@ -1319,6 +1319,34 @@
   return 1;
 }
 
+=head2 _delete_all_recursive 
+
+internal function to delete all related tables recursively with loop
+protection.
+
+=cut
+sub _delete_all_recursive {
+  my ($self) = shift;
+  my ($visited) = @_;
+
+  # delete from my tables that depend on me
+  my $source = $self->result_source;
+  foreach my $rel ($source->relationships) {
+	  my $relinfo = $source->relationship_info($rel);
+
+	  # debug
+	  #my $revinfo = $source->reverse_relationship_info($rel);
+	  #use Data::Dumper;
+	  #warn("rel=$rel relinfo=" . Dumper($relinfo) . " revinfo=" . Dumper($revinfo));
+
+	  next unless $relinfo->{attrs}{cascade_delete};
+	  next if( $visited->{$rel} );
+	  $visited->{$rel} = 1;
+	  $self->search_related($rel)->_delete_all_recursive($visited);
+  }
+  $_->delete for $self->all;
+}
+
 =head2 delete_all
 
 =over 4
@@ -1333,10 +1361,14 @@
 will run DBIC cascade triggers, while L</delete> will not.
 
 =cut
-
 sub delete_all {
   my ($self) = @_;
-  $_->delete for $self->all;
+
+  # delete all my dependent tables recursively careful to avoid loops
+  $self->_delete_all_recursive({});
+
+  #$_->delete for $self->all;
+
   return 1;
 }
 
_______________________________________________
List: http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class
IRC: irc.perl.org#dbix-class
SVN: http://dev.catalyst.perl.org/repos/bast/DBIx-Class/
Searchable Archive: http://www.grokbase.com/group/dbix-class@lists.scsys.co.uk

Reply via email to