3da77f16f56cdfb4edfed43985d369ceb300d899
[dbsrgits/DBIx-Class.git] / t / storage / savepoints.t
1 use strict;
2 use warnings;
3
4 use Test::More;
5 use Test::Exception;
6 use DBIx::Class::_Util qw(modver_gt_or_eq sigwarn_silencer);
7
8 use lib qw(t/lib);
9 use DBICTest;
10
11 {
12   package # moar hide
13     DBICTest::SVPTracerObj;
14
15   use base 'DBIx::Class::Storage::Statistics';
16
17   sub query_start { 'do notning'}
18   sub callback { 'dummy '}
19
20   for my $svpcall (map { "svp_$_" } qw(begin rollback release)) {
21     no strict 'refs';
22     *$svpcall = sub { $_[0]{uc $svpcall}++ };
23   }
24 }
25
26 my $env2optdep = {
27   DBICTEST_PG => 'test_rdbms_pg',
28   DBICTEST_MYSQL => 'test_rdbms_mysql',
29 };
30
31 my $schema;
32
33 for ('', keys %$env2optdep) { SKIP: {
34
35   my $prefix;
36
37   if ($prefix = $_) {
38     my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
39
40     skip ("Skipping tests with $prefix: set \$ENV{${prefix}_DSN} _USER and _PASS", 1)
41       unless $dsn;
42
43     skip ("Testing with ${prefix}_DSN needs " . DBIx::Class::Optional::Dependencies->req_missing_for( $env2optdep->{$prefix} ), 1)
44       unless  DBIx::Class::Optional::Dependencies->req_ok_for($env2optdep->{$prefix});
45
46     $schema = DBICTest::Schema->connect ($dsn,$user,$pass,{ auto_savepoint => 1 });
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     });
65   }
66   else {
67     $prefix = 'SQLite Internal DB';
68     $schema = DBICTest->init_schema( no_populate => 1, auto_savepoint => 1 );
69   }
70
71   note "Testing $prefix";
72
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   };
79   my $sg = Scope::Guard->new(sub {
80     $schema->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states;
81   });
82   $schema->storage->debugobj (my $stats = DBICTest::SVPTracerObj->new);
83   $schema->storage->debug (1);
84
85   $schema->resultset('Artist')->create({ name => 'foo' });
86
87   $schema->txn_begin;
88
89   my $arty = $schema->resultset('Artist')->find(1);
90
91   my $name = $arty->name;
92
93   # First off, test a generated savepoint name
94   $schema->svp_begin;
95
96   cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
97
98   $arty->update({ name => 'Jheephizzy' });
99
100   $arty->discard_changes;
101
102   cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
103
104   # Rollback the generated name
105   # Active: 0
106   $schema->svp_rollback;
107
108   cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
109
110   $arty->discard_changes;
111
112   cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
113
114   $arty->update({ name => 'Jheephizzy'});
115
116   # Active: 0 1
117   $schema->svp_begin('testing1');
118
119   $arty->update({ name => 'yourmom' });
120
121   # Active: 0 1 2
122   $schema->svp_begin('testing2');
123
124   $arty->update({ name => 'gphat' });
125   $arty->discard_changes;
126   cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
127
128   # Active: 0 1 2
129   # Rollback doesn't DESTROY the savepoint, it just rolls back to the value
130   # at its conception
131   $schema->svp_rollback('testing2');
132   $arty->discard_changes;
133   cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
134
135   # Active: 0 1 2 3
136   $schema->svp_begin('testing3');
137   $arty->update({ name => 'coryg' });
138
139   # Active: 0 1 2 3 4
140   $schema->svp_begin('testing4');
141   $arty->update({ name => 'watson' });
142
143   # Release 3, which implicitly releases 4
144   # Active: 0 1 2
145   $schema->svp_release('testing3');
146
147   $arty->discard_changes;
148   cmp_ok($arty->name, 'eq', 'watson', 'release left data');
149
150   # This rolls back savepoint 2
151   # Active: 0 1 2
152   $schema->svp_rollback;
153
154   $arty->discard_changes;
155   cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
156
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');
162
163   $schema->txn_commit;
164
165   is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
166
167   # And now to see if txn_do will behave correctly
168   $schema->txn_do (sub {
169     my $artycp = $arty;
170
171     $schema->txn_do (sub {
172       $artycp->name ('Muff');
173       $artycp->update;
174     });
175
176     eval {
177       $schema->txn_do (sub {
178         $artycp->name ('Moff');
179         $artycp->update;
180         $artycp->discard_changes;
181         is($artycp->name,'Moff','Value updated in nested transaction');
182         $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
183       });
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
197   is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
198
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');
204
205   cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
206
207   cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
208
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 SKIP: {
232   skip "Reading inexplicably fails on very old replicated DBD::SQLite<1.33", 1 if (
233     $ENV{DBICTEST_VIA_REPLICATED}
234       and
235     $prefix eq 'SQLite Internal DB'
236       and
237     ! modver_gt_or_eq('DBD::SQLite', '1.33')
238   );
239
240   ok($ars->search({ name => 'in_outer_transaction' })->first,
241     'commit from outer transaction');
242   ok($ars->search({ name => 'in_outer_transaction2' })->first,
243     'second commit from outer transaction');
244   ok($ars->search({ name => 'in_inner_transaction' })->first,
245     'commit from inner transaction');
246   is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
247     undef,
248     'rollback from inner transaction';
249 }
250
251 ### cleanupz
252   $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") });
253 }}
254
255 done_testing;
256
257 END {
258   local $SIG{__WARN__} = sigwarn_silencer( qr/Internal transaction state of handle/ )
259     unless modver_gt_or_eq('DBD::SQLite', '1.33');
260   eval { $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") }) } if defined $schema;
261   undef $schema;
262 }