Really fix SQLite savepoints unlike the shortsighted 398215b1
[dbsrgits/DBIx-Class.git] / t / storage / savepoints.t
1 BEGIN { do "./t/lib/ANFANG.pm" or die ( $@ || $! ) }
2
3 use strict;
4 use warnings;
5
6 use Test::More;
7 use Test::Exception;
8 use DBIx::Class::Optional::Dependencies;
9 use DBIx::Class::_Util qw(sigwarn_silencer scope_guard);
10
11 use DBICTest;
12
13 {
14   package # moar hide
15     DBICTest::SVPTracerObj;
16
17   use base 'DBIx::Class::Storage::Statistics';
18
19   sub query_start { 'do notning'}
20   sub callback { 'dummy '}
21
22   for my $svpcall (map { "svp_$_" } qw(begin rollback release)) {
23     no strict 'refs';
24     *$svpcall = sub { $_[0]{uc $svpcall}++ };
25   }
26 }
27
28 my $env2optdep = {
29   DBICTEST_PG => 'test_rdbms_pg',
30   DBICTEST_MYSQL => 'test_rdbms_mysql',
31 };
32
33 my $schema;
34
35 for ('', keys %$env2optdep) { SKIP: {
36
37   my $prefix;
38
39   if ($prefix = $_) {
40
41     DBIx::Class::Optional::Dependencies->skip_without($env2optdep->{$prefix});
42
43     my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
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   # can not use local() due to an unknown number of storages
73   # (think replicated)
74   my $orig_states = { map
75     { $_ => $schema->storage->$_ }
76     qw(debugcb debugobj debug)
77   };
78   my $sg = scope_guard {
79     $schema->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states;
80   };
81   $schema->storage->debugobj (my $stats = DBICTest::SVPTracerObj->new);
82   $schema->storage->debug (1);
83
84   $schema->resultset('Artist')->create({ name => 'foo' });
85
86   $schema->txn_begin;
87
88   my $arty = $schema->resultset('Artist')->find(1);
89
90   my $name = $arty->name;
91
92   # First off, test a generated savepoint name
93   $schema->svp_begin;
94
95   cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
96
97   $arty->update({ name => 'Jheephizzy' });
98
99   $arty->discard_changes;
100
101   cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
102
103   # Rollback the generated name
104   # Active: 0
105   $schema->svp_rollback;
106
107   cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
108
109   $arty->discard_changes;
110
111   cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
112
113   $arty->update({ name => 'Jheephizzy'});
114
115   # Active: 0 1
116   $schema->svp_begin('testing1');
117
118   $arty->update({ name => 'yourmom' });
119
120   # Active: 0 1 2
121   $schema->svp_begin('testing2');
122
123   $arty->update({ name => 'gphat' });
124   $arty->discard_changes;
125   cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
126
127   # Active: 0 1 2
128   # Rollback doesn't DESTROY the savepoint, it just rolls back to the value
129   # at its conception
130   $schema->svp_rollback('testing2');
131   $arty->discard_changes;
132   cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
133
134   # Active: 0 1 2 3
135   $schema->svp_begin('testing3');
136   $arty->update({ name => 'coryg' });
137
138   # Active: 0 1 2 3 4
139   $schema->svp_begin('testing4');
140   $arty->update({ name => 'watson' });
141
142   # Release 3, which implicitly releases 4
143   # Active: 0 1 2
144   $schema->svp_release('testing3');
145
146   $arty->discard_changes;
147   cmp_ok($arty->name, 'eq', 'watson', 'release left data');
148
149   # This rolls back savepoint 2
150   # Active: 0 1 2
151   $schema->svp_rollback;
152
153   $arty->discard_changes;
154   cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
155
156   # Rollback the original savepoint, taking us back to the beginning, implicitly
157   # rolling back savepoint 1 and 2
158   $schema->svp_rollback('savepoint_0');
159   $arty->discard_changes;
160   cmp_ok($arty->name, 'eq', 'foo', 'rolled back to start');
161
162   $schema->txn_commit;
163
164   is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
165
166   # And now to see if txn_do will behave correctly
167   $schema->txn_do (sub {
168     my $artycp = $arty;
169
170     $schema->txn_do (sub {
171       $artycp->name ('Muff');
172       $artycp->update;
173     });
174
175     eval {
176       $schema->txn_do (sub {
177         $artycp->name ('Moff');
178         $artycp->update;
179         $artycp->discard_changes;
180         is($artycp->name,'Moff','Value updated in nested transaction');
181         $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
182       });
183     };
184
185     ok ($@,'Nested transaction failed (good)');
186
187     $arty->discard_changes;
188
189     is($arty->name,'Muff','auto_savepoint rollback worked');
190
191     $arty->name ('Miff');
192
193     $arty->update;
194   });
195
196   is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
197
198   $arty->discard_changes;
199
200   is($arty->name,'Miff','auto_savepoint worked');
201
202   cmp_ok($stats->{'SVP_BEGIN'},'==',7,'Correct number of savepoints created');
203
204   cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
205
206   cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
207
208 ### test originally written for SQLite exclusively (git blame -w -C -M)
209   # test two-phase commit and inner transaction rollback from nested transactions
210   my $ars = $schema->resultset('Artist');
211
212   $schema->txn_do(sub {
213     $ars->create({ name => 'in_outer_transaction' });
214     $schema->txn_do(sub {
215       $ars->create({ name => 'in_inner_transaction' });
216     });
217     ok($ars->search({ name => 'in_inner_transaction' })->first,
218       'commit from inner transaction visible in outer transaction');
219     throws_ok {
220       $schema->txn_do(sub {
221         $ars->create({ name => 'in_inner_transaction_rolling_back' });
222         die 'rolling back inner transaction';
223       });
224     } qr/rolling back inner transaction/, 'inner transaction rollback executed';
225     $ars->create({ name => 'in_outer_transaction2' });
226   });
227
228   is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
229
230   ok($ars->search({ name => 'in_outer_transaction' })->first,
231     'commit from outer transaction');
232   ok($ars->search({ name => 'in_outer_transaction2' })->first,
233     'second commit from outer transaction');
234   ok($ars->search({ name => 'in_inner_transaction' })->first,
235     'commit from inner transaction');
236   is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
237     undef,
238     'rollback from inner transaction';
239
240   # make sure a fresh txn will work after above
241   $schema->storage->txn_do(sub { ok "noop" } );
242
243 ### cleanupz
244   $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") });
245 }}
246
247 done_testing;
248
249 END {
250   eval { $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") }) } if defined $schema;
251   undef $schema;
252 }