File Coverage

blib/lib/DBIx/Migration/Classes.pm
Criterion Covered Total %
statement 18 162 11.1
branch 0 72 0.0
condition 0 15 0.0
subroutine 6 22 27.2
pod 5 5 100.0
total 29 276 10.5


line stmt bran cond sub pod time code
1             package DBIx::Migration::Classes;
2              
3 1     1   23751 use 5.008009;
  1         4  
  1         33  
4 1     1   7 use strict;
  1         1  
  1         44  
5 1     1   6 use warnings;
  1         6  
  1         40  
6              
7 1     1   16970 use DBI;
  1         22601  
  1         91  
8 1     1   1208 use Module::Collect;
  1         3219  
  1         26  
9 1     1   951 use Data::Dumper;
  1         9487  
  1         2460  
10              
11             our $VERSION = '0.02';
12              
13             ################################################################################
14              
15             sub new
16             {
17 0     0 1   my ($class, @args) = @_;
18 0           my $self = bless {}, $class;
19 0           return $self->_init(@args);
20             }
21              
22             sub _test
23             {
24 0     0     my ($self) = @_;
25 0           my @tests = (
26             # [,]
27            
28             # up
29             ['NONE', 'HEAD'],
30             ['MyTestChanges::CreateTableUser', 'HEAD'],
31             ['MyTestChanges::CreateTableUser', 'MyTestChanges::AddUserName'],
32             ['MyTestChanges::AddUserName', 'HEAD'],
33            
34             # down
35             ['HEAD', 'NONE'],
36             ['HEAD', 'MyTestChanges::CreateTableUser'],
37             ['MyTestChanges::AddUserName', 'MyTestChanges::CreateTableUser'],
38             ['HEAD', 'MyTestChanges::AddUserName'],
39            
40             # same
41             ['NONE','NONE'],
42             ['HEAD','HEAD'],
43             ['MyTestChanges::CreateTableUser','MyTestChanges::CreateTableUser'],
44             ['MyTestChanges::AddUserName','MyTestChanges::AddUserName'],
45             );
46 0           my $t = 0;
47 0           foreach my $test (@tests) {
48 0           _info("\n---( $t )---------------------------------------------\n");
49 0           _info("--- $test->[0] .. $test->[1]\n\n");
50 0           $self->migrate(undef, $test->[0], $test->[1]);
51 0           $t++;
52             }
53             }
54              
55             sub migrate
56             {
57 0     0 1   my ($self, $targetstate, $_state, $_targetstate) = @_;
58 0 0         return 1 unless scalar @{$self->{'changeclasses'}};
  0            
59              
60 0           my $state = $self->state();
61            
62             # for testing purposes
63 0 0         $state = $_state if defined $_state;
64 0 0         $targetstate = $_targetstate if defined $_targetstate;
65              
66             # state = the name of the changeclass which changes were last APPLIED
67             # OR "NONE" if no changeclasses were applied
68             #
69             # targetstate = the name of the changeclass which is the last one to be APPLIED
70            
71 0           my $first_pos = $self->_get_position_of_changeclass($state);
72 0           my $last_pos = $self->_get_position_of_changeclass($targetstate);
73 0 0         return 1 if $first_pos eq $last_pos;
74              
75 0           my $dir = ($first_pos <=> $last_pos); # up = -1, down = 1, same = 0
76              
77 0 0         $state = $self->{'changeclasses'}->[$first_pos == -1 ? 0 : $first_pos]->[0];
78 0           $targetstate = $self->{'changeclasses'}->[$last_pos ]->[0];
79              
80 0           _info("migrating database from state $state to $targetstate...\n");
81              
82 0           my @changes = (); # actual database changes perfored by the changeclasses
83 0 0         if ($dir == 0) { # same
    0          
    0          
84 0           _info("- doing changes from ".$self->{'changeclasses'}->[$first_pos]->[0]."\n");
85 0           push @changes, $self->{'changeclasses'}->[$first_pos]->[1]->get_changes();
86             }
87             elsif ($dir == -1) { # up
88 0           for (my $p = $first_pos + 1; $p <= $last_pos; $p++) {
89 0           _info("- doing changes from ".$self->{'changeclasses'}->[$p]->[0]."\n");
90 0           push @changes, $self->{'changeclasses'}->[$p]->[1]->get_changes();
91             }
92             }
93             elsif ($dir == 1) { # down
94 0           for (my $p = $first_pos; $p > $last_pos; $p--) {
95 0           _info("- undoing changes from ".$self->{'changeclasses'}->[$p]->[0]."\n");
96 0           push @changes, $self->{'changeclasses'}->[$p]->[1]->get_changes('undo');
97             }
98 0 0         $targetstate = 'NONE' if $last_pos == -1;
99             }
100            
101 0 0         if (defined $_state) {
102 0           _info("database is now in state '".$self->state()."'\n");
103 0           _info("not applying changes, because this is a test\n");
104 0           return;
105             }
106            
107             # actually perform all changes
108 0           map { $self->_perform_change($_) } @changes;
  0            
109              
110 0           $self->_set_state($targetstate);
111 0           _info("database is now in state '".$self->state()."'\n");
112             }
113              
114             sub errstr
115             {
116 0     0 1   my ($self) = @_;
117 0           die "Error: error() method not yet implemented.\n";
118             }
119              
120             sub state
121             {
122 0     0 1   my ($self) = @_;
123 0           my $sql = 'select version from `'.$self->{'db-meta-tablename'}.'` limit 1';
124 0           my $sth = $self->_get_dbh()->prepare($sql);
125 0 0         $sth->execute() or die("Error: Failed to read meta status: $! $@\n");
126 0           my $meta = $sth->fetchrow_arrayref();
127 0 0         unless ($meta) {
128             # set state to 'NONE'
129 0 0         $self->_query('insert into `meta` values ("NONE")')
130             or die "Error: failed to set meta table version.\n";
131             }
132 0 0         return ($meta ? $meta->[0] : 'NONE');
133             }
134              
135             sub changes
136             {
137 0     0 1   my ($self) = @_;
138 0           die "Error: changes() method not yet implemented.\n";
139             }
140              
141             ################################################################################
142              
143             sub _init
144             {
145 0     0     my ($self, %opts) = @_;
146            
147 0   0       $self->{'namespaces'} = $opts{'namespaces'} || die "Error: no namespace(s) supplied.\n";
148              
149 0           $self->{'changeclasses'} = {}; # =>
150 0           $self->_collect_changeclasses();
151            
152 0   0       $self->{'db-name'} = $opts{'dbname'} || die "Error: no database name supplied.\n";
153 0   0       $self->{'db-user'} = $opts{'dbuser'} || 'root';
154 0   0       $self->{'db-password'} = $opts{'dbpassword'} || '';
155 0   0       $self->{'db-host'} = $opts{'dbhost'} || 'localhost';
156 0   0       $self->{'db-engine'} = $opts{'dbengine'} || 'mysql';
157            
158 0           $self->{'db-meta-tablename'} = 'meta';
159            
160 0           $self->{'dbh'} = undef;
161 0           $self->_init_database_metatable();
162            
163 0           return $self;
164             }
165              
166             sub _set_state
167             {
168 0     0     my ($self, $state) = @_;
169 0 0         $self->_query('update `'.$self->{'db-meta-tablename'}.'` set version = "'.$state.'"')
170             or die "Error: failed to update meta table.\n";
171 0           return 1;
172             }
173              
174             sub _perform_change
175             {
176 0     0     my ($self, $change) = @_;
177 0           my ($action, %opts) = @{$change};
  0            
178             #print Dumper($change);
179              
180 0 0         if ($action eq 'create_table') {
    0          
    0          
    0          
181 0 0         $self->_query('create table `'.$opts{'name'}.'` (`dummy` tinyint null)')
182             or die "Error: failed to create table '$opts{'name'}': ".$self->_get_dbh()->errstr."\n";
183             }
184             elsif ($action eq 'drop_table') {
185 0 0         $self->_query('drop table `'.$opts{'name'}.'`')
186             or die "Error: failed to drop table '$opts{'name'}': ".$self->_get_dbh()->errstr."\n";
187             }
188             elsif ($action eq 'alter_table_add_column') {
189 0 0         $self->_query('alter table `'.$opts{'tablename'}.'` add column `'.$opts{'name'}.'` '.$opts{'type'})
190             or die "Error: failed to add column '$opts{'name'}': ".$self->_get_dbh()->errstr."\n";
191             }
192             elsif ($action eq 'alter_table_drop_column') {
193 0 0         $self->_query('alter table `'.$opts{'tablename'}.'` drop column `'.$opts{'name'}.'`')
194             or die "Error: failed to drop column '$opts{'name'}': ".$self->_get_dbh()->errstr."\n";
195             }
196             else {
197 0           die "Error: action '$action' (change type) not yet implemented/supported.\n";
198             }
199 0           return 1;
200             }
201              
202             sub _query
203             {
204 0     0     my ($self, $sql) = @_;
205 0           my $sth = $self->_get_dbh()->prepare($sql);
206 0           return $sth->execute();
207             }
208              
209             sub _get_position_of_changeclass
210             {
211 0     0     my ($self, $classname) = @_;
212 0 0         return -1 if $classname eq 'NONE';
213 0 0         return scalar @{$self->{'changeclasses'}} - 1 if $classname eq 'HEAD';
  0            
214 0           foreach my $p (0.. scalar @{$self->{'changeclasses'}}) {
  0            
215 0 0         return $p if $self->{'changeclasses'}->[$p]->[0] eq $classname;
216             }
217 0           die "Error: failed to find change class '$classname'.\n";
218             }
219              
220             sub _collect_changeclasses
221             {
222 0     0     my ($self) = @_;
223            
224             # find all classes
225 0           my $classes = {};
226 0           foreach my $path (@INC) {
227             #print "$path:\n";
228 0           foreach my $namespace (@{$self->{'namespaces'}}) {
  0            
229             #print " $namespace:\n";
230 0           my $c = Module::Collect->new(path => $path, prefix => $namespace, multiple => 0);
231 0           for my $module (@{$c->modules}) {
  0            
232 0           my $classname = $module->package;
233 0 0         next if exists $classes->{$classname};
234 0           $module->require;
235 0           eval('$classes->{$classname} = '.$classname.'->new()');
236 0           $classes->{$classname}->perform(); # register actual changes (no effect in db)
237 0 0         die "Error: error while loading change class '$classname': $! $@\n" if $@;
238             }
239             }
240             }
241              
242             # store classes in execute order
243 0           $self->{'changeclasses'} = []; # [,], ...
244            
245             # find first one with after()=""
246 0           foreach my $classname (keys %{$classes}) {
  0            
247 0           my $class = $classes->{$classname};
248 0 0         if ($class->after() eq '') {
249 0           $self->{'changeclasses'}->[0] = [ $classname, $class ];
250 0           delete $classes->{$classname};
251 0           last;
252             }
253             }
254 0           while (scalar keys %{$classes}) {
  0            
255             # find one coming after last one
256 0           my $found = 0;
257 0           foreach my $classname (keys %{$classes}) {
  0            
258 0           my $class = $classes->{$classname};
259 0 0         if ($class->after() eq $self->{'changeclasses'}->[-1]->[0]) {
260 0           push @{$self->{'changeclasses'}}, [ $classname, $class ];
  0            
261 0           delete $classes->{$classname};
262 0           $found = 1;
263 0           last;
264             }
265             }
266 0 0         die "Error: failed to find a successor change class for '".
267             $self->{'changeclasses'}->[-1]->[0]."'.\n"
268             unless $found;
269             }
270             #print Dumper($self);
271             }
272              
273             sub _get_dbh
274             {
275 0     0     my ($self) = @_;
276 0 0 0       if (!defined $self->{'dbh'} || !$self->{'dbh'}->ping()) {
277 0           _info("(re)connecting to database...\n");
278 0           $self->{'dbh'} = $self->_connect_db()
279             }
280 0           return $self->{'dbh'};
281             }
282              
283             sub _connect_db
284             {
285 0     0     my ($self) = @_;
286 0 0         my $dbh =
287             DBI->connect(
288             "DBI:".$self->{'db-engine'}.":".$self->{'db-name'}.":".$self->{'db-host'},
289             $self->{'db-user'}, $self->{'db-password'},
290             { PrintError => 0 },
291             )
292             or die("Error: Could not connect to database: $! $@\n");
293            
294 0 0         die("Error: could not connect to database: $! $@\n")
295             unless defined $dbh;
296 0           return $dbh;
297             }
298              
299             sub _init_database_metatable
300             {
301 0     0     my ($self) = @_;
302             # create db tables if nessessary
303 0           my $sth = $self->_get_dbh()->table_info("", $self->{'db-name'}, $self->{'db-meta-tablename'}, "TABLE");
304 0 0         unless ($sth->fetch()) {
305 0           _info("creating metatable in database...\n");
306 0           my $sql = 'create table `'.$self->{'db-meta-tablename'}.'` (`version` varchar(255))';
307 0 0         $self->_query($sql) or die "Error: failed to create meta table: $! $@\n";
308             }
309             }
310              
311             sub _info
312             {
313 0     0     my (@msg) = @_;
314 0           print STDERR join('', @msg);
315             }
316              
317             ################################################################################
318             1;
319             __END__