File Coverage

blib/lib/ORLite/Migrate.pm
Criterion Covered Total %
statement 106 120 88.3
branch 31 58 53.4
condition 12 30 40.0
subroutine 15 15 100.0
pod 0 2 0.0
total 164 225 72.8


line stmt bran cond sub pod time code
1             package ORLite::Migrate;
2              
3             # See POD at end of file for documentation
4              
5 4     4   163112 use 5.006;
  4         16  
  4         166  
6 4     4   1570 use strict;
  4         10  
  4         159  
7 4     4   33 use Carp ();
  4         16  
  4         73  
8 4     4   23 use File::Spec 3.2701 ();
  4         151  
  4         84  
9 4     4   20 use File::Path 2.04 ();
  4         86  
  4         89  
10 4     4   21 use File::Basename ();
  4         5  
  4         58  
11 4     4   1988 use Params::Util 0.37 ();
  4         14136  
  4         105  
12 4     4   6562 use DBI 1.58 ();
  4         40663  
  4         129  
13 4     4   2854 use DBD::SQLite 1.21 ();
  4         20884  
  4         98  
14 4     4   1982 use ORLite 1.28 ();
  4         10274  
  4         112  
15              
16 4     4   29 use vars qw{$VERSION @ISA};
  4         6  
  4         277  
17             BEGIN {
18 4     4   9 $VERSION = '1.10';
19 4         6158 @ISA = 'ORLite';
20             }
21              
22             sub import {
23 2   33 2   955 my $class = ref $_[0] || $_[0];
24              
25             # Check for debug mode
26 2         6 my $DEBUG = 0;
27 2 50 33     56 if ( defined Params::Util::_STRING($_[-1]) and $_[-1] eq '-DEBUG' ) {
28 0         0 $DEBUG = 1;
29 0         0 pop @_;
30             }
31              
32             # Check params and apply defaults
33 2         5 my %params;
34 2 50       29 if ( defined Params::Util::_STRING($_[1]) ) {
    50          
35             # Migrate needs at least two params
36 0         0 Carp::croak("ORLite::Migrate must be invoked in HASH form");
37             } elsif ( Params::Util::_HASH($_[1]) ) {
38 2         4 %params = %{ $_[1] };
  2         15  
39             } else {
40 0         0 Carp::croak("Missing, empty or invalid params HASH");
41             }
42 2 100 66     41 if ( $params{timeline} and not defined $params{create} ) {
43 1         3 $params{create} = 1;
44             }
45 2 50       9 $params{create} = $params{create} ? 1 : 0;
46 2 50 33     27 unless (
      33        
47             defined Params::Util::_STRING($params{file})
48             and (
49             $params{create}
50             or
51             -f $params{file}
52             )
53             ) {
54 0         0 Carp::croak("Missing or invalid file param");
55             }
56 2 50       10 unless ( defined $params{readonly} ) {
57 2 50       11 $params{readonly} = $params{create} ? 0 : ! -w $params{file};
58             }
59 2 50       9 unless ( defined $params{tables} ) {
60 2         7 $params{tables} = 1;
61             }
62 2 50       7 unless ( defined $params{package} ) {
63 2         11 $params{package} = scalar caller;
64             }
65 2 50       79 unless ( Params::Util::_CLASS($params{package}) ) {
66 0         0 Carp::croak("Missing or invalid package class");
67             }
68              
69 2 50 33     181 unless (
      33        
      66        
70             Params::Util::_DRIVER($params{timeline}, 'ORLite::Migrate::Timeline')
71             or
72             ($params{timeline} and -d $params{timeline} and -r $params{timeline})
73             ) {
74 0         0 Carp::croak("Missing or invalid timeline");
75             }
76              
77             # We don't support readonly databases
78 2 50       490 if ( $params{readonly} ) {
79 0         0 Carp::croak("ORLite::Migrate does not support readonly databases");
80             }
81              
82             # Get the schema version
83 2         104 my $file = File::Spec->rel2abs($params{file});
84 2         24 my $created = ! -f $params{file};
85 2 50       8 if ( $created ) {
86             # Create the parent directory
87 2         195 my $dir = File::Basename::dirname($file);
88 2 50       75 unless ( -d $dir ) {
89 0         0 my @dirs = File::Path::mkpath( $dir, { verbose => 0 } );
90 0 0       0 $class->prune(@dirs) if $params{prune};
91             }
92 2 50       33 $class->prune($file) if $params{prune};
93             }
94 2         39 my $dsn = "dbi:SQLite:$file";
95 2         21 my $dbh = DBI->connect($dsn);
96 2         5051 my $version = $dbh->selectrow_arrayref('pragma user_version')->[0];
97 2         510 $dbh->disconnect;
98              
99             # We're done with the prune setting now
100 2         7 $params{prune} = 0;
101              
102             # Handle the migration class
103 2 100       75 if ( Params::Util::_DRIVER($params{timeline}, 'ORLite::Migrate::Timeline') ) {
104 1         300 my $timeline = $params{timeline}->new(
105             dbh => DBI->connect($dsn),
106             );
107 1         20 $timeline->upgrade( $params{user_version} );
108              
109             } else {
110 1         83 my $timeline = File::Spec->rel2abs($params{timeline});
111 1         4 my @plan = plan( $params{timeline}, $version );
112              
113             # Execute the migration plan
114 1 50       4 if ( @plan ) {
115             # Does the migration plan reach the required destination
116 1         3 my $destination = $version + scalar(@plan);
117 1 50 33     9 if (
118             exists $params{user_version}
119             and
120             $destination != $params{user_version}
121             ) {
122 0         0 die "Schema migration destination user_version mismatch (got $destination, wanted $params{user_version})";
123             }
124              
125             # Load the modules needed for the migration
126 1         890 require Probe::Perl;
127 1         2823 require File::pushd;
128 1         35500 require IPC::Run3;
129              
130             # Locate our Perl interpreter
131 1         17100 my $perl = Probe::Perl->find_perl_interpreter;
132 1 50       20 unless ( $perl ) {
133 0         0 Carp::croak("Unable to locate your perl executable");
134             }
135              
136             # Execute each script
137 1         5 my $pushd = File::pushd::pushd($timeline);
138 1         208 foreach my $patch ( @plan ) {
139 3         30 my $stdin = "$file\n";
140 3 50       12 if ( $DEBUG ) {
141 0         0 print STDERR "Applying schema patch $patch...\n";
142             }
143 3 50       33 my $ok = IPC::Run3::run3( [ $perl, $patch ], \$stdin, \undef, $DEBUG ? undef : \undef );
144 3 50 33     3934524 if ( ! $ok or $? != 0 ) {
145 0         0 Carp::croak("Migration patch $patch failed, database in unknown state");
146             }
147             }
148              
149             # Migration complete, set user_version to new state
150 1         60 $dbh = DBI->connect($dsn);
151 1         958 $dbh->do("pragma user_version = $destination");
152 1         43565 $dbh->disconnect;
153             }
154             }
155              
156             # Hand off to the regular constructor
157 2 50       221 $class->SUPER::import(
158             \%params,
159             $DEBUG ? '-DEBUG' : ()
160             );
161             }
162              
163              
164              
165              
166              
167             #####################################################################
168             # Simple Methods
169              
170             sub patches {
171 3     3 0 847 my $dir = shift;
172              
173             # Find all files in a directory
174 3         11 local *DIR;
175 3 50       122 opendir( DIR, $dir ) or die "opendir: $!";
176 3 50       68 my @files = readdir( DIR ) or die "readdir: $!";
177 3 50       50 closedir( DIR ) or die "closedir: $!";
178              
179             # Filter to get the patch set
180 3         8 my @patches = ();
181 3         9 foreach ( @files ) {
182 15 100       61 next unless /^migrate-(\d+)\.pl$/;
183 9         28 $patches["$1"] = $_;
184             }
185              
186 3         20 return @patches;
187             }
188              
189             sub plan {
190 2     2 0 807 my $directory = shift;
191 2         5 my $version = shift;
192              
193             # Find the list of patches
194 2         9 my @patches = patches( $directory );
195              
196             # Assemble the plan by integer stepping forwards
197             # until we run out of timeline hits.
198 2         8 my @plan = ();
199 2         9 while ( $patches[++$version] ) {
200 5         15 push @plan, $patches[$version];
201             }
202              
203 2         10 return @plan;
204             }
205              
206             1;
207              
208             __END__