File Coverage

blib/lib/DBIx/VersionedDDL.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package DBIx::VersionedDDL;
2 2     2   108517 use Moose;
  0            
  0            
3             use MooseX::Attribute::ENV;
4             use DBI;
5             use DBI::Const::GetInfoType;
6             use Carp;
7             use File::Basename;
8              
9             with 'MooseX::Object::Pluggable';
10              
11             has 'user' => (is => 'ro', isa => 'Str', required => 0);
12             has 'pass' => (is => 'ro', isa => 'Str', required => 0);
13             has 'dsn' => (is => 'ro', isa => 'Str', required => 0);
14             has 'ddl_dir' => (is => 'ro', isa => 'Str', required => 1);
15             has 'debug' => (is => 'ro', isa => 'Str', required => 0, default => 0);
16             has 'dbh' => (is => 'rw', isa => 'DBI::db', required => 0);
17              
18             has 'script_processor' => (
19             is => 'rw',
20             isa => 'Str',
21             required => 1,
22             traits => ['ENV'],
23             env_package_prefix => 1,
24             default => 'DefaultScriptProcessor',
25             );
26              
27             =head1 NAME
28              
29             DBIx::VersionedDDL - Upgrade and downgrade database schemas to a specified version.
30              
31             =head1 VERSION
32              
33             Version 0.17
34              
35             =cut
36              
37             our $VERSION = '0.17';
38              
39             =head1 SYNOPSIS
40              
41             This module is a utility that can upgrade or downgrade a schema.
42              
43             use DBIx::VersionedDDL;
44              
45             my $sv = DBIx::VersionedDDL->new(
46             user => 'scott',
47             pass => 'tiger',
48             dsn => 'DBI:Oracle:orcl',
49             ddl_dir => '/home/jdoe/ddl'
50             );
51            
52             # Migrate the schema to version 7.
53             $sv->migrate(7);
54            
55             or alternatively:
56              
57             use DBIx::VersionedDDL;
58              
59             my $dbh = DBI->connect('DBI:Oracle:orcl', 'scott', 'tiger');
60            
61             my $sv = DBIx::VersionedDDL->new(
62             dbh => $dbh,
63             ddl_dir => '/home/jdoe/ddl'
64             );
65            
66             # Migrate the schema to version 7.
67             $sv->migrate(7);
68            
69             DBIx::VersiondDDL installs a version table (schema_version) in
70             the schema that stores the current version number of this
71             schema. The developer or DBA who maintains the schema will place
72             a series of upgrade and downgrade scripts in a ddl directory.
73             The scripts will use the following naming convention:
74              
75             +-----------------+-------------------+
76             | Upgrade Scripts | Downgrade Scripts |
77             +-----------------+-------------------+
78             | upgrade1.sql | downgrade1.sql |
79             | upgrade2.sql | downgrade2.sql |
80             | upgrade3.sql | downgrade3.sql |
81             | upgrade4.sql | downgrade4.sql |
82             | .... | .... |
83             +-----------------+-------------------+
84              
85             Each downgrade script reverses the changes of the upgrade
86             script. The scripts to run are determined by the value in the
87             version table If the current schema is at version 5 and the
88             administrator wishes to upgrade to version 10, upgrade scripts
89             6-10 would be run by the utility, and the version table entry
90             will be incremented at each step.
91              
92             If the administrator wishes to downgrade to version 6, the
93             utility will run downgrade scripts 10-7.
94              
95             This utility expects SQL statements to be separated by
96             semi-colons by default, but an explicit separator can be
97             specified.
98              
99             =head2 Leaving the schema in an indeterminate state.
100              
101             DDL statements are not transactionally aware, so you can not
102             roll back if there is an error in your DDL. You will need to
103             manually resolve such errors.
104              
105             Any error will be saved to the schema_version table. The version
106             number is set to the script that was running when the error was
107             encountered.
108              
109             =head2 Supplying an active database handle
110              
111             The database handle will autocommit any updates to the schema
112             tables. If an active handle is supplied, it should not be shared
113             with any external transactions. The best approach is to clone
114             an active handle before passing it to the utility:
115              
116             my $dbh = DBI->connect('DBI:Oracle:orcl', 'scott', 'tiger');
117            
118             my $sv = DBIx::VersionedDDL->new(
119             dbh => $dbh->clone,
120             ddl_dir => '/home/jdoe/ddl'
121             );
122              
123             The schema_version table has three columns:
124              
125             +---------+---------------------------------------------------+
126             | Column | Description |
127             +---------+---------------------------------------------------+
128             | version | The current version of the schema |
129             | status | The status of the script that updated the version |
130             | | record. It will be either 'success' or 'error' |
131             | message | Any message generated when an error occurs |
132             +---------+---------------------------------------------------+
133              
134             =head1 METHODS
135              
136             =head2 new
137              
138             The following attributes can be supplied at creation time by passing
139             values to the new method.
140              
141             =over 4
142              
143             =item * B<user>. The database user account
144              
145             =item * B<pass>. The user password.
146              
147             =item * B<dsn>. The database DSN
148              
149             =item * B<ddl_dir>. The directory that hosts the migration scripts
150              
151             =item * B<debug>. Whether debug messages are shown
152              
153             =item * B<dbh>. An active database handle. This can be used as an alternative
154             to the user, pass and dsn parameters
155              
156             =item * B<script_processor>. Optional. A plugin that processes the migration
157             scripts. See L</PROVIDING YOUR OWN PROCESSOR VIA A PLUGIN>
158              
159             =back
160              
161             =head2 migrate
162              
163             Migrate from the current schema version to the specified one:
164              
165             $sv->migrate(7);
166            
167             If a version is not provided, the schema will be upgraded (or downgraded!)
168             to the maximum version specified by upgrade(n).sql:
169              
170             $sv->migrate
171            
172             =head2 get_message
173              
174             Returns the message value in the schema_version table
175              
176             =cut
177              
178             sub migrate {
179             my $self = shift;
180             my $requested_version = shift;
181              
182             unless (defined $requested_version) {
183             $requested_version = $self->_get_max_version;
184             }
185              
186             croak 'No version provided' unless $requested_version =~ /^\d+$/;
187              
188             $self->_create_version_table unless $self->_version_table_exists;
189              
190             my $current_version = $self->_get_current_version;
191              
192             croak "Invalid version" unless ($requested_version =~ /^\d+$/);
193             croak "Invalid DDL directory" unless (-d $self->ddl_dir);
194              
195             return 1 if $requested_version == $current_version;
196              
197             my ($version, $script, $stop_version, $prefix);
198             if ($requested_version > $current_version) {
199             $prefix = 'upgrade';
200             $version = $current_version + 1;
201             $stop_version = $requested_version;
202             } else {
203             $prefix = 'downgrade';
204             $version = $current_version;
205             $stop_version = $requested_version;
206             }
207              
208             eval {
209             while (1)
210             {
211             printf("%s%s.sql\n", $prefix, $version) if $self->debug;
212             $script = sprintf("%s%s.sql", $prefix, $version);
213             $self->_run($script);
214              
215             if ($prefix eq 'upgrade') {
216             $self->_update_version($version, 'success', undef);
217             last if $version == $stop_version;
218             $version++;
219             } else {
220             $version--;
221             $self->_update_version($version, 'success', undef);
222             last if $version == $stop_version;
223             }
224             }
225             };
226              
227             if ($@) {
228             my $error = $@;
229             $version-- if $prefix eq 'downgrade';
230             $self->_update_version($version, 'error',
231             basename($script) . ': ' . $error);
232             return 0;
233             }
234              
235             return 1;
236             }
237              
238             sub _get_max_version {
239             my $self = shift;
240             my $version = 0;
241             foreach my $file (glob($self->ddl_dir . "/upgrade*.sql")) {
242             if ($file =~ /upgrade(\d+).sql/) {
243             $version = $1 if $1 > $version;
244             }
245             }
246             return $version;
247             }
248              
249             # Set up the application
250             sub BUILD {
251             my $self = shift;
252              
253             unless ($self->dbh) {
254             croak "No database connect info" unless $self->dsn;
255              
256             my $dbh =
257             DBI->connect($self->dsn, $self->user, $self->pass, {RaiseError => 1})
258             || croak DBI::errstr;
259              
260             $self->dbh($dbh);
261             }
262              
263             # Set this just in case it was unset in an external dbh
264             $self->dbh->{AutoCommit} = 1;
265             $self->dbh->{RaiseError} = 1;
266             $self->dbh->{PrintError} = 0;
267              
268             croak "No DDL dir: " . $self->ddl_dir unless -d $self->ddl_dir;
269              
270             $self->load_plugins($self->script_processor,);
271             }
272              
273             # Determine whether or not the version table exists
274             sub _version_table_exists {
275             my $self = shift;
276             my $table = 'schema_version';
277             my $driver = lc $self->_get_driver;
278             my $schema;
279              
280             # Oracle stores its tables in upper case while other dbs such as MySQL
281             # use the case specified at table creation time
282             if ($driver eq 'oracle') {
283             $table = uc $table;
284             $schema = uc $self->dbh->get_info($GetInfoType{SQL_USER_NAME});
285             } elsif ($driver eq 'mysql') {
286             $schema =
287             (split /:/, $self->dbh->get_info($GetInfoType{SQL_DATA_SOURCE_NAME}))
288             [2];
289             }
290              
291             my $sth = $self->dbh->table_info(undef, $schema, $table, 'TABLE');
292              
293             while (my $table_info = $sth->fetchrow_hashref()) {
294             # Depending on whether FetchHashKeyName has been set to lower or
295             # upper case, we should check both versions
296              
297             my $table_col =
298             (defined $table_info->{table_name})
299             ? 'table_name'
300             : 'TABLE_NAME';
301              
302             if ($table_info->{$table_col} eq $table) {
303             return 1;
304             }
305             }
306              
307             return 0;
308             }
309              
310             # create the version table
311             sub _create_version_table {
312             my $self = shift;
313              
314             # 3 Columns:
315             # * version: The schema version number
316             # * message: Any message generated by the schema upgrade/downgrade
317             # Generally only populated following a migration error.
318             # * status: The status following the upgrade. Either "success" or
319             # error
320             my $sql = q{
321             create table schema_version(
322             version integer,
323             message varchar(4000),
324             status varchar(8)
325             )
326             };
327             $self->dbh->do($sql);
328             $sql = q{insert into schema_version(version) values(0)};
329             $self->dbh->do($sql);
330             }
331              
332             # Determine the current schema version
333             sub _get_current_version {
334             my $self = shift;
335             my $sql = q{select version from schema_version};
336              
337             my ($version) = $self->dbh->selectrow_array($sql);
338             return $version;
339             }
340              
341             sub get_message {
342             my $self = shift;
343             my $sql = q{select message from schema_version};
344              
345             my ($message) = $self->dbh->selectrow_array($sql);
346             return $message;
347             }
348              
349             # Run the specified SQL script
350             sub _run {
351             my $self = shift;
352             my $script = shift || croak "No script";
353             $script = $self->ddl_dir . '/' . $script;
354             croak "Cannot find $script" unless -f $script;
355              
356             my @statements = $self->process_script($script);
357              
358             foreach my $statement (@statements) {
359             next if $statement =~ /^\s*$/;
360             next unless $statement;
361             $self->dbh->do($statement);
362             }
363             }
364              
365             # get the database type that we're connected to.
366             sub _get_driver {
367             my $self = shift;
368             return $self->dbh->get_info($GetInfoType{SQL_DBMS_NAME});
369             }
370              
371             # update the version table
372             sub _update_version {
373             my $self = shift;
374             my $version = shift;
375             my $status = shift || 'success';
376             my $message = shift;
377              
378             my $sql = q{
379             update schema_version
380             set version = ?,
381             status = ?,
382             message = ?
383             };
384              
385             $self->dbh->do($sql, undef, $version, $status, $message);
386             }
387              
388             =head1 PROVIDING YOUR OWN PROCESSOR VIA A PLUGIN
389              
390             You can supply your own functionality to parse migration scripts
391             via a plugin. The plugin must contain a I<process_script> method
392             that takes a script name as an argument and returns an array of
393             SQL statements. The name of your plugin can either be set in the
394             environment variable I<SCRIPT_PROCESSOR> or the I<script_processor>
395             attribute as part of the constructor. If your plugin is called
396             DBIx::VersionedDDL::Plugin::MyProcessor, then SCRIPT_PROCESSOR should
397             be set to I<MyProcessor>.
398              
399             For an example, refer to the source of L<DBIx::VersionedDDL::Plugin::DefaultScriptProcessor>
400              
401             =head2 Populating plugin attributes
402              
403             Any attributes should be populated once the Versioned object is created:
404              
405             my $sv = DBIx::VersionedDDL->new(
406             user => 'scott',
407             pass => 'tiger',
408             dsn => 'DBI:Oracle:orcl',
409             ddl_dir => '/home/jdoe/ddl'
410             );
411            
412             $sv->separator('/');
413              
414             =head1 SCHEMA DEFINITION
415              
416             The definition of a schema can differ between database products. For
417             Oracle a schema is a:
418              
419             collection of database objects, including logical structures
420             such as tables, views, sequences, stored procedures, synonyms,
421             indexes, clusters, and database links. A schema has the name of
422             the user who controls it.
423            
424             In this context, the user parameter should be the owner of the schema.
425             With other databases, such as MySQL, the schema is analogous to a
426             database. The user parameter should be an account that has full
427             privileges to the database so that it can apply the DDL and update
428             the schema_version table.
429              
430             =head1 SEE ALSO
431              
432             L<migrate_schema> is a script supplied with this distribution that
433             supports the invocation of DBIx::VersionedDDL from the command line.
434              
435             migrate_schema -user=scott -pass=tiger -dsn=DBI:Oracle:orcl \
436             -version=5 -ddl_dir=/my/ddl/dir
437            
438             For more information:
439              
440             man migrate_schema
441              
442             =head1 AUTHOR
443              
444             Dan Horne, C<< <dhorne at cpan.org> >>
445              
446             =head1 ACKNOWLEDGEMENTS
447              
448             Plugin functionality added by Jiri Pavlovsky.
449              
450             =head1 BUGS
451              
452             Please report any bugs or feature requests to
453             C<bug-dbix-versionedddl at rt.cpan.org>, or through the web
454             interface at
455             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=DBIx-VersionedDDL>.
456             I will be notified, and then you'll automatically be notified of
457             progress on your bug as I make changes.
458              
459             =head1 SUPPORT
460              
461             You can find documentation for this module with the perldoc command.
462              
463             perldoc DBIx::VersionedDDL
464              
465              
466             You can also look for information at:
467              
468             =over 4
469              
470             =item * RT: CPAN's request tracker
471              
472             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=DBIx-VersionedDDL>
473              
474             =item * AnnoCPAN: Annotated CPAN documentation
475              
476             L<http://annocpan.org/dist/DBIx-VersionedDDL>
477              
478             =item * CPAN Ratings
479              
480             L<http://cpanratings.perl.org/d/DBIx-VersionedDDL>
481              
482             =item * Search CPAN
483              
484             L<http://search.cpan.org/dist/DBIx-VersionedDDL/>
485              
486             =back
487              
488             =head1 COPYRIGHT & LICENSE
489              
490             Copyright 2009-2010 Dan Horne.
491              
492             This program is free software; you can redistribute it and/or modify it under
493             the same terms as Perl itself.
494              
495              
496             =cut
497              
498             1; # End of DBIx::VersionedDDL