Avoid infinite loop if save point does not exist
[dbsrgits/DBIx-Class.git] / t / storage / savepoints.t
CommitLineData
c0329273 1BEGIN { do "./t/lib/ANFANG.pm" or die ( $@ || $! ) }
2
ddf66ced 3use strict;
4use warnings;
5
6use Test::More;
cf1d16d8 7use Test::Exception;
18a2903b 8use DBIx::Class::Optional::Dependencies;
66c817df 9use DBIx::Class::_Util qw(sigwarn_silencer scope_guard);
f5f0cb1d 10use Scalar::Util 'weaken';
c0329273 11
2cfc22dd 12use DBICTest;
13
14{
15 package # moar hide
16 DBICTest::SVPTracerObj;
17
18 use base 'DBIx::Class::Storage::Statistics';
19
20 sub query_start { 'do notning'}
21 sub callback { 'dummy '}
22
23 for my $svpcall (map { "svp_$_" } qw(begin rollback release)) {
24 no strict 'refs';
25 *$svpcall = sub { $_[0]{uc $svpcall}++ };
26 }
27}
ddf66ced 28
ae1d3ea1 29my $env2optdep = {
cf1d16d8 30 DBICTEST_PG => 'test_rdbms_pg',
ae1d3ea1 31 DBICTEST_MYSQL => 'test_rdbms_mysql',
32};
ddf66ced 33
ae1d3ea1 34my $schema;
ddf66ced 35
cf1d16d8 36for ('', keys %$env2optdep) { SKIP: {
ddf66ced 37
cf1d16d8 38 my $prefix;
ddf66ced 39
cf1d16d8 40 if ($prefix = $_) {
ddf66ced 41
3cff955a 42 DBIx::Class::Optional::Dependencies->skip_without($env2optdep->{$prefix});
ddf66ced 43
3cff955a 44 my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
cf1d16d8 45
c7e85630 46 $schema = DBICTest::Schema->connect ($dsn,$user,$pass,{ auto_savepoint => 1 });
cf1d16d8 47
48 my $create_sql;
49 $schema->storage->ensure_connected;
50 if ($schema->storage->isa('DBIx::Class::Storage::DBI::Pg')) {
51 $create_sql = "CREATE TABLE artist (artistid serial PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10))";
52 $schema->storage->dbh->do('SET client_min_messages=WARNING');
53 }
54 elsif ($schema->storage->isa('DBIx::Class::Storage::DBI::mysql')) {
55 $create_sql = "CREATE TABLE artist (artistid INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10)) ENGINE=InnoDB";
56 }
57 else {
58 skip( 'Untested driver ' . $schema->storage, 1 );
59 }
60
61 $schema->storage->dbh_do (sub {
62 $_[1]->do('DROP TABLE IF EXISTS artist');
63 $_[1]->do($create_sql);
64 });
ae1d3ea1 65 }
66 else {
cf1d16d8 67 $prefix = 'SQLite Internal DB';
68 $schema = DBICTest->init_schema( no_populate => 1, auto_savepoint => 1 );
ae1d3ea1 69 }
ddf66ced 70
ae1d3ea1 71 note "Testing $prefix";
ddf66ced 72
b74b15b0 73 # can not use local() due to an unknown number of storages
74 # (think replicated)
75 my $orig_states = { map
76 { $_ => $schema->storage->$_ }
77 qw(debugcb debugobj debug)
78 };
bbf6a9a5 79 my $sg = scope_guard {
b74b15b0 80 $schema->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states;
bbf6a9a5 81 };
b74b15b0 82 $schema->storage->debugobj (my $stats = DBICTest::SVPTracerObj->new);
83 $schema->storage->debug (1);
ddf66ced 84
ae1d3ea1 85 $schema->resultset('Artist')->create({ name => 'foo' });
ddf66ced 86
ae1d3ea1 87 $schema->txn_begin;
ddf66ced 88
ae1d3ea1 89 my $arty = $schema->resultset('Artist')->find(1);
ddf66ced 90
ae1d3ea1 91 my $name = $arty->name;
ddf66ced 92
ae1d3ea1 93 # First off, test a generated savepoint name
94 $schema->svp_begin;
ddf66ced 95
ae1d3ea1 96 cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
ddf66ced 97
ae1d3ea1 98 $arty->update({ name => 'Jheephizzy' });
ddf66ced 99
ae1d3ea1 100 $arty->discard_changes;
ddf66ced 101
ae1d3ea1 102 cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
ddf66ced 103
ae1d3ea1 104 # Rollback the generated name
105 # Active: 0
106 $schema->svp_rollback;
ddf66ced 107
ae1d3ea1 108 cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
ddf66ced 109
ae1d3ea1 110 $arty->discard_changes;
ddf66ced 111
ae1d3ea1 112 cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
ddf66ced 113
ae1d3ea1 114 $arty->update({ name => 'Jheephizzy'});
ddf66ced 115
ae1d3ea1 116 # Active: 0 1
117 $schema->svp_begin('testing1');
ddf66ced 118
ae1d3ea1 119 $arty->update({ name => 'yourmom' });
ddf66ced 120
ae1d3ea1 121 # Active: 0 1 2
122 $schema->svp_begin('testing2');
ddf66ced 123
ae1d3ea1 124 $arty->update({ name => 'gphat' });
125 $arty->discard_changes;
126 cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
cf1d16d8 127
ae1d3ea1 128 # Active: 0 1 2
129 # Rollback doesn't DESTROY the savepoint, it just rolls back to the value
3334d204 130 # at its conception
ae1d3ea1 131 $schema->svp_rollback('testing2');
132 $arty->discard_changes;
133 cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
ddf66ced 134
ae1d3ea1 135 # Active: 0 1 2 3
136 $schema->svp_begin('testing3');
137 $arty->update({ name => 'coryg' });
cf1d16d8 138
ae1d3ea1 139 # Active: 0 1 2 3 4
140 $schema->svp_begin('testing4');
141 $arty->update({ name => 'watson' });
ddf66ced 142
ae1d3ea1 143 # Release 3, which implicitly releases 4
144 # Active: 0 1 2
145 $schema->svp_release('testing3');
cf1d16d8 146
ae1d3ea1 147 $arty->discard_changes;
148 cmp_ok($arty->name, 'eq', 'watson', 'release left data');
cf1d16d8 149
ae1d3ea1 150 # This rolls back savepoint 2
151 # Active: 0 1 2
152 $schema->svp_rollback;
cf1d16d8 153
ae1d3ea1 154 $arty->discard_changes;
155 cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
ddf66ced 156
ae1d3ea1 157 # Rollback the original savepoint, taking us back to the beginning, implicitly
158 # rolling back savepoint 1 and 2
159 $schema->svp_rollback('savepoint_0');
160 $arty->discard_changes;
161 cmp_ok($arty->name, 'eq', 'foo', 'rolled back to start');
ddf66ced 162
ae1d3ea1 163 $schema->txn_commit;
164
cf1d16d8 165 is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
166
ae1d3ea1 167 # And now to see if txn_do will behave correctly
168 $schema->txn_do (sub {
65d35121 169 my $artycp = $arty;
170
ae1d3ea1 171 $schema->txn_do (sub {
65d35121 172 $artycp->name ('Muff');
173 $artycp->update;
ae1d3ea1 174 });
ddf66ced 175
176 eval {
177 $schema->txn_do (sub {
65d35121 178 $artycp->name ('Moff');
179 $artycp->update;
180 $artycp->discard_changes;
181 is($artycp->name,'Moff','Value updated in nested transaction');
ae1d3ea1 182 $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
183 });
ddf66ced 184 };
185
186 ok ($@,'Nested transaction failed (good)');
187
188 $arty->discard_changes;
189
190 is($arty->name,'Muff','auto_savepoint rollback worked');
191
192 $arty->name ('Miff');
193
194 $arty->update;
195 });
196
cf1d16d8 197 is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
198
ae1d3ea1 199 $arty->discard_changes;
200
201 is($arty->name,'Miff','auto_savepoint worked');
202
203 cmp_ok($stats->{'SVP_BEGIN'},'==',7,'Correct number of savepoints created');
ddf66ced 204
ae1d3ea1 205 cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
ddf66ced 206
ae1d3ea1 207 cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
ddf66ced 208
cf1d16d8 209### test originally written for SQLite exclusively (git blame -w -C -M)
210 # test two-phase commit and inner transaction rollback from nested transactions
211 my $ars = $schema->resultset('Artist');
212
213 $schema->txn_do(sub {
214 $ars->create({ name => 'in_outer_transaction' });
215 $schema->txn_do(sub {
216 $ars->create({ name => 'in_inner_transaction' });
217 });
218 ok($ars->search({ name => 'in_inner_transaction' })->first,
219 'commit from inner transaction visible in outer transaction');
220 throws_ok {
221 $schema->txn_do(sub {
222 $ars->create({ name => 'in_inner_transaction_rolling_back' });
223 die 'rolling back inner transaction';
224 });
225 } qr/rolling back inner transaction/, 'inner transaction rollback executed';
226 $ars->create({ name => 'in_outer_transaction2' });
227 });
228
229 is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
230
231 ok($ars->search({ name => 'in_outer_transaction' })->first,
232 'commit from outer transaction');
233 ok($ars->search({ name => 'in_outer_transaction2' })->first,
234 'second commit from outer transaction');
235 ok($ars->search({ name => 'in_inner_transaction' })->first,
236 'commit from inner transaction');
237 is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
238 undef,
239 'rollback from inner transaction';
66c817df 240
241 # make sure a fresh txn will work after above
242 $schema->storage->txn_do(sub { ok "noop" } );
cf1d16d8 243
f5f0cb1d 244### Make sure non-existend savepoint release doesn't infloop itself
245 {
246 weaken( my $s = $schema );
247
248 throws_ok {
249 $s->storage->txn_do(sub { $s->svp_release('wibble') })
250 } qr/Savepoint 'wibble' does not exist/,
251 "Calling svp_release on a non-existant savepoint throws expected error"
252 ;
253 }
254
cf1d16d8 255### cleanupz
b74b15b0 256 $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") });
ae1d3ea1 257}}
ddf66ced 258
ae1d3ea1 259done_testing;
ddf66ced 260
65d35121 261END {
b74b15b0 262 eval { $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") }) } if defined $schema;
65d35121 263 undef $schema;
264}