sub svp_rollback { die "Virtual method!" }
+=head2 svp_generate_name
+
+Generates a name for the next savepoint. Defaults to 'savepoint_$count',
+where count is the number of current savepoints + 1.
+
+=cut
+
+sub svp_generate_name { die "Virtual method!" }
+
=for comment
=head2 txn_scope_guard
qw/_connect_info _dbi_connect_info _dbh _sql_maker _sql_maker_opts
_conn_pid _conn_tid disable_sth_caching on_connect_do
on_disconnect_do transaction_depth unsafe _dbh_autocommit
- auto_savepoint/
+ auto_savepoint savepoints/
);
__PACKAGE__->cursor_class('DBIx::Class::Storage::DBI::Cursor');
$new->transaction_depth(0);
$new->_sql_maker_opts({});
+ $new->{savepoints} = [];
$new->{_in_dbh_do} = 0;
$new->{_dbh_gen} = 0;
sub svp_begin {
my ($self, $name) = @_;
-
- $self->throw_exception("You failed to provide a savepoint name!") if !$name;
- if($self->{transaction_depth} == 0) {
- warn("Can't use savepoints without a transaction.");
- return 0;
- }
+ $name = $self->_svp_generate_name
+ unless defined $name;
+
+ $self->throw_exception ("You can't use savepoints outside a transaction")
+ if $self->{transaction_depth} == 0;
+
+ $self->throw_exception ("Your Storage implementation doesn't support savepoints")
+ unless $self->can('_svp_begin');
+
+ push @{ $self->{savepoints} }, $name;
- if(!$self->can('_svp_begin')) {
- warn("Your Storage implementation doesn't support savepoints!");
- return 0;
- }
$self->debugobj->svp_begin($name) if $self->debug;
- $self->_svp_begin($name);
+
+ return $self->_svp_begin($name);
}
sub svp_release {
my ($self, $name) = @_;
- $self->throw_exception("You failed to provide a savepoint name!") if !$name;
+ $self->throw_exception ("You can't use savepoints outside a transaction")
+ if $self->{transaction_depth} == 0;
- if($self->{transaction_depth} == 0) {
- warn("Can't use savepoints without a transaction.");
- return 0;
- }
+ $self->throw_exception ("Your Storage implementation doesn't support savepoints")
+ unless $self->can('_svp_release');
+
+ if (defined $name) {
+ $self->throw_exception ("Savepoint '$name' does not exist")
+ unless grep { $_ eq $name } @{ $self->{savepoints} };
- if(!$self->can('_svp_release')) {
- warn("Your Storage implementation doesn't support savepoint releasing!");
- return 0;
+ # Dig through the stack until we find the one we are releasing. This keeps
+ # the stack up to date.
+ my $svp;
+
+ do { $svp = pop @{ $self->{savepoints} } } while $svp ne $name;
+ } else {
+ $name = pop @{ $self->{savepoints} };
}
+
$self->debugobj->svp_release($name) if $self->debug;
- $self->_svp_release($name);
+
+ return $self->_svp_release($name);
}
sub svp_rollback {
my ($self, $name) = @_;
- $self->throw_exception("You failed to provide a savepoint name!") if !$name;
+ $self->throw_exception ("You can't use savepoints outside a transaction")
+ if $self->{transaction_depth} == 0;
- if($self->{transaction_depth} == 0) {
- warn("Can't use savepoints without a transaction.");
- return 0;
- }
+ $self->throw_exception ("Your Storage implementation doesn't support savepoints")
+ unless $self->can('_svp_rollback');
- if(!$self->can('_svp_rollback')) {
- warn("Your Storage implementation doesn't support savepoints!");
- return 0;
+ if (defined $name) {
+ # If they passed us a name, verify that it exists in the stack
+ unless(grep({ $_ eq $name } @{ $self->{savepoints} })) {
+ $self->throw_exception("Savepoint '$name' does not exist!");
+ }
+
+ # Dig through the stack until we find the one we are releasing. This keeps
+ # the stack up to date.
+ while(my $s = pop(@{ $self->{savepoints} })) {
+ last if($s eq $name);
+ }
+ # Add the savepoint back to the stack, as a rollback doesn't remove the
+ # named savepoint, only everything after it.
+ push(@{ $self->{savepoints} }, $name);
+ } else {
+ # We'll assume they want to rollback to the last savepoint
+ $name = $self->{savepoints}->[-1];
}
+
$self->debugobj->svp_rollback($name) if $self->debug;
- $self->_svp_rollback($name);
+
+ return $self->_svp_rollback($name);
+}
+
+sub _svp_generate_name {
+ my ($self) = @_;
+
+ return 'savepoint_'.scalar(@{ $self->{'savepoints'} });
}
sub txn_begin {
# for AutoCommit users
$self->dbh->begin_work;
} elsif ($self->auto_savepoint) {
- $self->svp_begin ("savepoint_$self->{transaction_depth}");
+ $self->svp_begin;
}
$self->{transaction_depth}++;
}
}
elsif($self->{transaction_depth} > 1) {
$self->{transaction_depth}--;
- $self->svp_release ("savepoint_$self->{transaction_depth}")
+ $self->svp_release
if $self->auto_savepoint;
}
}
elsif($self->{transaction_depth} > 1) {
$self->{transaction_depth}--;
if ($self->auto_savepoint) {
- $self->svp_rollback ("savepoint_$self->{transaction_depth}");
- $self->svp_release ("savepoint_$self->{transaction_depth}");
+ $self->svp_rollback;
+ $self->svp_release;
}
}
else {
use strict;
-use warnings;
+use warnings;
use Test::More;
use lib qw(t/lib);
plan skip_all => 'Set $ENV{DBICTEST_MYSQL_DSN}, _USER and _PASS to run this test'
unless ($dsn && $user);
-plan tests => 9;
+plan tests => 7;
my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
},
};
-$schema->txn_begin();
+$schema->txn_begin;
my $arty = $schema->resultset('Artist')->find(1);
-my $name = $arty->name();
+my $name = $arty->name;
-$schema->svp_begin('savepoint1');
-
-cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
+$schema->storage->_svp_begin ("mysavepoint");
$arty->update({ name => 'Jheephizzy' });
-$arty->discard_changes();
-
-cmp_ok($arty->name(), 'eq', 'Jheephizzy', 'Name changed');
+$arty->discard_changes;
-$schema->svp_rollback('savepoint1');
+cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
-cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
+$schema->storage->_svp_rollback ("mysavepoint");
-$arty->discard_changes();
+$arty->discard_changes;
-cmp_ok($arty->name(), 'eq', $name, 'Name rolled back');
+cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
-$schema->txn_commit();
+$schema->txn_commit;
SKIP: {
my $mysql_version = $dbh->get_info( $GetInfoType{SQL_DBMS_VER} );
plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test'
. ' (note: creates and drops tables named artist and casecheck!)' unless ($dsn && $user);
-plan tests => 43;
+plan tests => 34;
DBICTest::Schema->load_classes( 'Casecheck' );
my $schema = DBICTest::Schema->connect($dsn, $user, $pass, { auto_savepoint => 1});
is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
}
-$schema->txn_begin();
+$schema->txn_begin;
my $arty = $schema->resultset('Artist')->find(1);
-my $name = $arty->name();
+my $name = $arty->name;
-$schema->svp_begin('savepoint1');
-
-cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
+$schema->storage->_svp_begin ("mysavepoint");
$arty->update({ name => 'Jheephizzy' });
-$arty->discard_changes();
-
-cmp_ok($arty->name(), 'eq', 'Jheephizzy', 'Name changed');
-
-$schema->svp_rollback('savepoint1');
-
-cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
-
-$arty->discard_changes();
-
-cmp_ok($arty->name(), 'eq', $name, 'Name rolled back');
-
-$schema->txn_commit();
-
-$schema->txn_do (sub {
- $schema->txn_do (sub {
- $arty->name ('Muff');
-
- $arty->update;
- });
-
- eval {
- $schema->txn_do (sub {
- $arty->name ('Moff');
-
- $arty->update;
-
- $arty->discard_changes;
-
- is($arty->name,'Moff','Value updated in nested transaction');
-
- $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
- });
- };
-
- ok ($@,'Nested transaction failed (good)');
-
- $arty->discard_changes;
-
- is($arty->name,'Muff','auto_savepoint rollback worked');
+$arty->discard_changes;
- $arty->name ('Miff');
+cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
- $arty->update;
- });
+$schema->storage->_svp_rollback ("mysavepoint");
$arty->discard_changes;
-is($arty->name,'Miff','auto_savepoint worked');
-
-cmp_ok($stats->{'SVP_BEGIN'},'==',3,'Correct number of savepoints created');
-
-cmp_ok($stats->{'SVP_RELEASE'},'==',2,'Correct number of savepoints released');
+cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
-cmp_ok($stats->{'SVP_ROLLBACK'},'==',2,'Correct number of savepoint rollbacks');
+$schema->txn_commit;
END {
if($dbh) {
--- /dev/null
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+use DBICTest::Stats;
+
+my ($create_sql, $dsn, $user, $pass);
+
+if (exists $ENV{DBICTEST_PG_DSN}) {
+ ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
+
+ $create_sql = "CREATE TABLE artist (artistid serial PRIMARY KEY, name VARCHAR(100), charfield CHAR(10))";
+} elsif (exists $ENV{DBICTEST_MYSQL_DSN}) {
+ ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/};
+
+ $create_sql = "CREATE TABLE artist (artistid INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), charfield CHAR(10)) ENGINE=InnoDB";
+} else {
+ plan skip_all => 'Set DBICTEST_(PG|MYSQL)_DSN _USER and _PASS if you want to run savepoint tests';
+}
+
+plan tests => 16;
+
+my $schema = DBICTest::Schema->connect ($dsn,$user,$pass,{ auto_savepoint => 1 });
+
+my $stats = DBICTest::Stats->new;
+
+$schema->storage->debugobj($stats);
+
+$schema->storage->debug(1);
+
+$schema->storage->dbh->do ($create_sql);
+
+$schema->resultset('Artist')->create({ name => 'foo' });
+
+$schema->txn_begin;
+
+my $arty = $schema->resultset('Artist')->find(1);
+
+my $name = $arty->name;
+
+# First off, test a generated savepoint name
+$schema->svp_begin;
+
+cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
+
+$arty->update({ name => 'Jheephizzy' });
+
+$arty->discard_changes;
+
+cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
+
+# Rollback the generated name
+# Active: 0
+$schema->svp_rollback;
+
+cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
+
+$arty->discard_changes;
+
+cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
+
+$arty->update({ name => 'Jheephizzy'});
+
+# Active: 0 1
+$schema->svp_begin('testing1');
+
+$arty->update({ name => 'yourmom' });
+
+# Active: 0 1 2
+$schema->svp_begin('testing2');
+
+$arty->update({ name => 'gphat' });
+$arty->discard_changes;
+cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
+# Active: 0 1 2
+# Rollback doesn't DESTROY the savepoint, it just rolls back to the value
+# at it's conception
+$schema->svp_rollback('testing2');
+$arty->discard_changes;
+cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
+
+# Active: 0 1 2 3
+$schema->svp_begin('testing3');
+$arty->update({ name => 'coryg' });
+# Active: 0 1 2 3 4
+$schema->svp_begin('testing4');
+$arty->update({ name => 'watson' });
+
+# Release 3, which implicitly releases 4
+# Active: 0 1 2
+$schema->svp_release('testing3');
+$arty->discard_changes;
+cmp_ok($arty->name, 'eq', 'watson', 'release left data');
+# This rolls back savepoint 2
+# Active: 0 1 2
+$schema->svp_rollback;
+$arty->discard_changes;
+cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
+
+# Rollback the original savepoint, taking us back to the beginning, implicitly
+# rolling back savepoint 1 and 2
+$schema->svp_rollback('savepoint_0');
+$arty->discard_changes;
+cmp_ok($arty->name, 'eq', 'foo', 'rolled back to start');
+
+$schema->txn_commit;
+
+# And now to see if txn_do will behave correctly
+
+$schema->txn_do (sub {
+ $schema->txn_do (sub {
+ $arty->name ('Muff');
+
+ $arty->update;
+ });
+
+ eval {
+ $schema->txn_do (sub {
+ $arty->name ('Moff');
+
+ $arty->update;
+
+ $arty->discard_changes;
+
+ is($arty->name,'Moff','Value updated in nested transaction');
+
+ $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
+ });
+ };
+
+ ok ($@,'Nested transaction failed (good)');
+
+ $arty->discard_changes;
+
+ is($arty->name,'Muff','auto_savepoint rollback worked');
+
+ $arty->name ('Miff');
+
+ $arty->update;
+ });
+
+$arty->discard_changes;
+
+is($arty->name,'Miff','auto_savepoint worked');
+
+cmp_ok($stats->{'SVP_BEGIN'},'==',7,'Correct number of savepoints created');
+
+cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
+
+cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
+
+END { $schema->storage->dbh->do ("DROP TABLE artist") if defined $schema }
+