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