File Coverage

blib/lib/Migraine.pm
Criterion Covered Total %
statement 27 185 14.5
branch 0 76 0.0
condition 0 14 0.0
subroutine 9 27 33.3
pod 13 16 81.2
total 49 318 15.4


line stmt bran cond sub pod time code
1             package Migraine;
2              
3 8     8   525354 use strict;
  8         18  
  8         475  
4              
5             our $VERSION = "0.54"; # ==> ALSO update the version in the pod text below!
6              
7 8     8   44 use Carp;
  8         14  
  8         711  
8 8     8   48 use DBI;
  8         20  
  8         295  
9 8     8   6950 use English qw(-no_match_vars);
  8         50773  
  8         54  
10              
11 8     8   4479 use constant MIGRATION_REGEX => qr/^(\d+)-.+\..+/;
  8         17  
  8         884  
12 8         448 use constant MIGRAINE_META_CREATION_SQL =>
13             "CREATE TABLE migraine_meta (name varchar(20),
14 8     8   42 value varchar(200))";
  8         11  
15 8         381 use constant MIGRAINE_META_UPGRADE_SQL =>
16             "ALTER TABLE migraine_meta ADD (name varchar(20),
17 8     8   41 value varchar(200))";
  8         22  
18 8         487 use constant MIGRAINE_META_REMOVE_OBSOLETE_SQL =>
19 8     8   43 "ALTER TABLE migraine_meta DROP version";
  8         12  
20 8         23460 use constant MIGRAINE_MIGRATIONS_CREATION_SQL =>
21             "CREATE TABLE migraine_migrations (id integer,
22 8     8   41 PRIMARY KEY (id))";
  8         10  
23              
24             our $SUPPORTED_METADATA_FORMAT = 2;
25              
26             sub new {
27 0     0 0   my ($class, $dsn, %params) = @_;
28 0           my $dbh = DBI->connect($dsn, $params{user},
29             $params{password},
30             $params{dbi_options});
31 0           my $attrs = { migrations_dir => "migrations",
32             %params,
33             dbh => $dbh };
34 0           bless $attrs, $class;
35             }
36              
37             sub dbh {
38 0     0 1   my ($self) = @_;
39 0           $self->{dbh};
40             }
41              
42             sub migraine_metadata_present {
43 0     0 1   my ($self) = @_;
44              
45 0           return $self->_check_table_exists("migraine_meta");
46             }
47              
48             sub migraine_metadata_usable {
49 0     0 1   my ($self) = @_;
50              
51 0           return ($self->migraine_metadata_version == $SUPPORTED_METADATA_FORMAT);
52             }
53              
54             sub _check_table_exists {
55 0     0     my ($self, $table_name) = @_;
56              
57 0           my @tables = $self->dbh->tables(undef, undef, $table_name);
58 0           return (scalar grep /$table_name/, @tables);
59             }
60              
61             sub migraine_metadata_version {
62 0     0 1   my ($self) = @_;
63              
64 0 0         if ($self->migraine_metadata_present) {
65 0           my ($old_raise, $old_print) = ($self->dbh->{RaiseError},
66             $self->dbh->{PrintError});
67 0           $self->dbh->{RaiseError} = $self->dbh->{PrintError} = 0;
68 0           my $sth = $self->dbh->prepare("SELECT value
69             FROM migraine_meta
70             WHERE name = 'metadata_version'");
71 0           my $r = $sth->execute;
72 0           $self->dbh->{RaiseError} = $old_raise;
73 0           $self->dbh->{PrintError} = $old_print;
74 0 0         if ($r) {
75 0           my $results = $sth->fetchrow_hashref;
76 0 0         if (defined $results->{value}) {
77 0           return 0 + $results->{value}; # Force number
78             }
79             else {
80 0           croak "Inconsistent or unknown migraine_meta";
81             }
82             }
83             else {
84 0           my $old_raise = $self->dbh->{RaiseError};
85 0           $self->dbh->{RaiseError} = 0;
86 0           $sth = $self->dbh->prepare("SELECT version FROM migraine_meta");
87 0           $r = $sth->execute;
88 0           $self->dbh->{RaiseError} = $old_raise;
89 0 0         if ($r) {
90 0           return 1;
91             }
92             else {
93 0           croak "Inconsistent or unknown migraine_meta";
94             }
95             }
96             }
97             else {
98 0           return 0;
99             }
100             }
101              
102             sub create_migraine_metadata {
103 0     0 1   my ($self) = @_;
104              
105 0 0         unless ($self->migraine_metadata_present) {
106 0           my $sth = $self->dbh->prepare(MIGRAINE_META_CREATION_SQL);
107 0 0         $sth->execute ||
108             croak "Couldn't create migraine_meta table: $DBI::errstr\n";
109 0           $sth = $self->dbh->prepare(MIGRAINE_MIGRATIONS_CREATION_SQL);
110 0 0         $sth->execute ||
111             croak "Couldn't create migraine_migrations table: $DBI::errstr\n";
112 0           $sth = $self->dbh->prepare("INSERT INTO migraine_meta (name, value)
113             VALUES (?, ?)");
114 0 0         $sth->execute('metadata_version', $SUPPORTED_METADATA_FORMAT) ||
115             croak "Couldn't insert migraine_meta information.";
116             }
117 0           return 1;
118             }
119              
120             sub upgrade_migraine_metadata {
121 0     0 1   my ($self) = @_;
122              
123 0           my $df = $self->migraine_metadata_version;
124 0 0         if ($df == 0) {
    0          
    0          
125 0           return $self->create_migraine_metadata;
126             }
127             elsif ($df == 1) {
128 0           my $dbh = $self->dbh;
129 0 0         $dbh->do(MIGRAINE_META_UPGRADE_SQL) ||
130             croak "Couldn't upgrade the migraine_meta table: $DBI::errstr\n";
131             # Store how many migrations had been applied and remove obsolete record
132 0           my $res = $dbh->selectall_arrayref("SELECT version FROM migraine_meta");
133 0           my $version = $res->[0]->[0];
134 0           $dbh->do("DELETE FROM migraine_meta"); # Remove obsolete record
135 0 0         $dbh->do(MIGRAINE_META_REMOVE_OBSOLETE_SQL) ||
136             croak "Couldn't remove obsolete fields from migraine_meta table: $DBI::errstr\n";
137 0           $dbh->do("INSERT INTO migraine_meta (name, value)
138             VALUES ('metadata_version', '2')");
139 0 0         $dbh->do(MIGRAINE_MIGRATIONS_CREATION_SQL) ||
140             croak "Couldn't create migraine_migrations table: $DBI::errstr\n";
141 0           for (my $v = 1; $v <= $version; $v++) {
142 0           $dbh->do("INSERT INTO migraine_migrations (id) VALUES ($v)");
143             }
144 0           return 1;
145             }
146             elsif ($df == 2) {
147 0           return 1;
148             }
149             else {
150 0           croak "Sorry, I don't know how to upgrade from version $df";
151             }
152             }
153              
154             sub latest_version {
155 0     0 1   my ($self) = @_;
156              
157 0           opendir D, $self->{migrations_dir};
158 0           my ($higher_version) = reverse sort { $a <=> $b }
  0            
159 0           map { $_ =~ MIGRATION_REGEX;
160 0           int($1); }
161 0           grep { $_ =~ MIGRATION_REGEX }
162             readdir D;
163 0           closedir D;
164 0 0         $higher_version || 0;
165             }
166              
167             sub current_version {
168 0     0 1   my ($self) = @_;
169              
170 0 0         if ($self->migraine_metadata_present) {
171 0           my $sth = $self->dbh->prepare("SELECT id FROM migraine_migrations
172             ORDER BY id DESC
173             LIMIT 1");
174 0 0 0       if (defined $sth && $sth->execute) {
175 0           $sth->bind_columns(\my $version);
176 0           $sth->fetch;
177 0   0       return $version || 0;
178             }
179             else {
180 0           croak "Can't query migraine_meta table?! ".$self->dbh->errstr;
181             }
182             }
183             else {
184 0           return 0;
185             }
186             }
187              
188             sub migration_applied {
189 0     0 1   my ($self, $migration_id) = @_;
190              
191 0 0         return unless $self->migraine_metadata_usable;
192 0           my $sth = $self->dbh->prepare("SELECT id FROM migraine_migrations
193             WHERE id = ?");
194 0 0 0       if (defined $sth && $sth->execute($migration_id)) {
195 0           $sth->bind_columns(\my $version);
196 0           $sth->fetch;
197 0 0 0       if (defined $version && $version == $migration_id) {
198 0           return 1;
199             }
200             }
201             else {
202 0           return 0;
203             }
204             }
205              
206             sub migrate {
207 0     0 1   my ($self, %user_params) = @_;
208 0           my %params = (no_act => 0, %user_params);
209              
210 0   0       my $up_to_version = $params{version} || $self->latest_version;
211              
212             # Create migraine metadata if it's not there
213 0 0         $self->create_migraine_metadata unless $params{no_act};
214              
215 0           for (my $cnt = 1; $cnt <= $up_to_version; $cnt++) {
216 0 0         next if $self->migration_applied($cnt);
217 0           $self->apply_migration($cnt, before_migrate => $params{before_migrate},
218             after_migrate => $params{after_migrate},
219             no_act => $params{no_act},
220             skip_missing_migrations =>
221             $params{skip_missing_migrations});
222             }
223             }
224              
225             sub _mark_migration_as_applied {
226 0     0     my ($self, $version) = @_;
227              
228 0           my $sth = $self->dbh->prepare("INSERT INTO migraine_migrations (id)
229             VALUES (?)");
230 0           $sth->execute($version);
231             }
232              
233             sub apply_migration {
234 0     0 1   my ($self, $version, %user_options) = @_;
235 0           my %options = (no_act => 0,
236             skip_missing_migrations => 0,
237             %user_options);
238              
239 0 0         if ($self->migration_applied($version)) {
240 0           croak "Migration $version already applied";
241             }
242              
243 0           my $contents;
244 0           eval {
245 0 0         if ($options{before_migrate}) {
246 0           $options{before_migrate}->($version,
247             $self->get_migration_path($version));
248             }
249 0           $contents = $self->get_migration($version);
250             };
251 0 0         if ($EVAL_ERROR) {
252 0 0         if ($options{skip_missing_migrations}) {
253 0           print STDERR "Skipping migration $version: $EVAL_ERROR";
254 0           return;
255             }
256             else {
257 0           die $EVAL_ERROR;
258             }
259             }
260              
261 0 0         if (defined $contents) {
262 0 0         unless ($options{no_act}) {
263 0           foreach my $query (split(/;\s*\n/, $contents)) {
264 0           my $sth = $self->dbh->prepare($query);
265 0 0         if ($sth) {
266 0           my $r;
267 0           eval { $r = $sth->execute };
  0            
268 0 0         $r || croak "Couldn't execute migration $version ($query): ".$self->dbh->errstr;
269             }
270             else {
271 0           croak "Can't prepare migration $version: ".$self->dbh->errstr;
272             }
273             }
274 0           $self->_mark_migration_as_applied($version);
275             }
276              
277 0 0         if ($options{after_migrate}) {
278 0           $options{after_migrate}->($version,
279             $self->get_migration_path($version));
280             }
281             }
282             }
283              
284             sub applied_migrations {
285 0     0 1   my ($self) = @_;
286              
287 0 0         if ($self->migraine_metadata_usable) {
288 0           my $res = $self->dbh->selectall_arrayref("SELECT id
289             FROM migraine_migrations
290             ORDER BY id");
291 0           return map { $_->[0] } @$res;
  0            
292             }
293             else {
294 0           return;
295             }
296             }
297              
298             sub applied_migration_ranges {
299 0     0 1   my ($self) = @_;
300              
301 0           my @ordered_migrations = $self->applied_migrations;
302 0 0         return if scalar @ordered_migrations == 0;
303 0           my $range_first_item = $ordered_migrations[0];
304 0           my $last_item = $range_first_item;
305 0           my @r = ();
306 0           foreach my $id (@ordered_migrations) {
307 0 0         if ($id > $last_item + 1) {
308 0 0         push @r,
309             ($last_item != $range_first_item ?
310             "$range_first_item-$last_item" :
311             "$range_first_item");
312 0           $range_first_item = $id;
313             }
314 0           $last_item = $id;
315             }
316              
317             # Take care of the last range/element
318 0 0         push @r,
319             ($last_item != $range_first_item ?
320             "$range_first_item-$last_item" :
321             "$range_first_item");
322              
323 0           return @r;
324             }
325              
326             sub get_migration_path {
327 0     0 0   my ($self, $version) = @_;
328              
329 0           opendir D, $self->{migrations_dir};
330 0           my @migrations = map { "$self->{migrations_dir}/$_" }
  0            
331             grep /^0*$version-.+\..+$/,
332             readdir D;
333 0           closedir D;
334              
335 0 0         @migrations || die "Can't find migration $version\n";
336 0 0         scalar @migrations == 1 || die "More than one migration '$version'?!\n";
337 0           return $migrations[0];
338             }
339              
340             sub get_migration {
341 0     0 0   my ($self, $version) = @_;
342              
343             # This will throw an exception if it's not there, there is more than one or
344             # whatever. So we can assume everything was right.
345 0           my $path = $self->get_migration_path($version);
346              
347 0           open F, $path;
348 0           my $contents = join("", );
349 0           close F;
350 0           return $contents;
351             }
352              
353             1;
354              
355             __END__