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