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