File Coverage

blib/lib/Module/Build/DB.pm
Criterion Covered Total %
statement 23 114 20.1
branch 4 58 6.9
condition 0 8 0.0
subroutine 5 13 38.4
pod 9 9 100.0
total 41 202 20.3


line stmt bran cond sub pod time code
1             package Module::Build::DB;
2              
3 3     3   51963 use strict;
  3         9  
  3         130  
4 3     3   19 use warnings;
  3         6  
  3         111  
5              
6 3     3   17 use base 'Module::Build';
  3         11  
  3         15754  
7             our $VERSION = '0.10';
8              
9             =head1 Name
10              
11             Module::Build::DB - Build, configure, and test database-backed applications
12              
13             =head1 Synopsis
14              
15             In F:
16              
17             use strict;
18             use Module::Build::DB;
19              
20             Module::Build::DB->new(
21             module_name => 'MyApp',
22             db_config_key => 'dbi',
23             context => 'test',
24             )->create_build_script;
25              
26             On the command-line:
27              
28             perl Build.PL
29             ./Build --db_super_user postgres
30             ./Build db --context test
31             ./Build test
32              
33             =head1 Description
34              
35             This module subclasses L to provide added functionality for
36             configuring, building, and testing database-backed applications. It uses a
37             simple Rails-style numbered migration scheme, although migration scripts are
38             written in pure SQL, not Perl.
39              
40             Frankly, this isn't a great module. Some reasons:
41              
42             =over
43              
44             =item *
45              
46             The numbered method of tracking migration dependencies has very little
47             flexibility.
48              
49             =item *
50              
51             Subclassing Module::Build is a really bad way to extend the build system,
52             because you can't really mix in other build features.
53              
54             =back
55              
56             Someday, I hope to fix the first issue by looking more closely at L
57             change
58             management|http://www.justatheory.com/computers/databases/change-management.html>,
59             and perhaps by adopting a L
60             approach|http://www.depesz.com/index.php/2010/08/22/versioning/>. The latter
61             problem I would likely solve by completely separating the migration code from
62             the build system, and then integrating as appropriate (hopefully Module::Build
63             will get proper plugins someday).
64              
65             But in the meantime, I have working code that depends on this simple
66             implementation (which does support L,
67             L and L), and I
68             want it to be easy for people to get at this dependency. So here we are.
69              
70             =cut
71              
72             ##############################################################################
73              
74             =head1 Class Interface
75              
76             =head2 Properties
77              
78             Module::Build::DB defines these properties in addition to those specified by
79             L. Note that these may be specified either in
80             F or on the command-line.
81              
82             =head3 context
83              
84             perl Build.PL --context test
85              
86             Specifies the context in which the build will run. The context associates the
87             build with a configuration file, and therefore must be named for a
88             configuration file your project. For example, to build in the "dev" context,
89             there must be a F file (or F or some other format supported
90             by L) in the F or F directory of your project.
91             Defaults to "test", which is also the only required context.
92              
93             =head3 db_client
94              
95             perl Build.PL --db_client /usr/local/pgsql/bin/pgsql
96              
97             Specifies the location of the database command-line client. Defaults to
98             F, F, or F, depending on the value of the DSN in the
99             context configuration file.
100              
101             =head3 drop_db
102              
103             ./Build db --drop_db 1
104              
105             Tells the L action to drop the database and build a new one. When this
106             property is set to a false value (the default), an existing database for the
107             current context will not be dropped, but it will be brought up-to-date by
108             C<./Build db>.
109              
110             =head3 db_config_key
111              
112             The config key under which DBI configuration is stored in the configuration
113             file. Defaults to "dbi". The keys that should be under this configuration key
114             are:
115              
116             =over
117              
118             =item * C
119              
120             =item * C
121              
122             =item * C
123              
124             =back
125              
126             =head3 db_super_user
127              
128             =head3 db_super_pass
129              
130             perl Build.PL --db_super_user root --db_super_pass s3cr1t
131              
132             Specifies a super user and password to be used to connect to the database.
133             This is important if you need to use a different database user to create and
134             update the database than to run your app. Most likely you'll use this for
135             production deployments. If not specified the user name and password from the
136             the context configuration file will be used.
137              
138             =head3 test_env
139              
140             ./Build db --test_env CATALYST_DEBUG=0 CATALYST_CONFIG=conf/test.json
141              
142             Optional hash reference of environment variables to set for the lifetime of
143             C<./Build test>. This can be useful for making Catalyst less verbose, for
144             example. Another use is to tell PostgreSQL where to find pgTAP functions when
145             they're installed in a schema outside the normal search path in your database:
146              
147             ./Build db --test_env PGOPTIONS='--search_path=tap,public'
148              
149             =head3 meta_table
150              
151             ./Build db --meta_table mymeta
152              
153             The name of the metadata table that Module::Build::DB uses to track migrations
154             in the database. Defaults to "metadata". Change if that name conflicts with
155             other objects in your application's database, but use only characters that
156             don't require quoting in the database (e.g., "my_meta" but not "my meta").
157              
158             =head3 replace_config
159              
160             Module::Build::DB->new(
161             module_name => 'MyApp',
162             db_config_key => 'dbi',
163             replace_config => 'conf/dev.json',
164             )->create_build_script;
165              
166             Set to a string or regular expression (using C) and, the C
167             file will be opened during C<./Build> and matching strings replaced with name
168             of the context configuration file. This is useful when deploying Catalyst
169             applications, for example, where your C file might have something
170             like this in it:
171              
172             __PACKAGE__->config(
173             name => 'MyApp',
174             'Plugin::ConfigLoader' => { file => 'conf/dev.json' },
175             );
176              
177             The C string would be replaced in the copy of the file in
178             F with the context configuration file name. Use a regular expression
179             if you want to cover a variety of values, as in:
180              
181             replace_config => qr{etc/[^.].json},
182              
183             =head3 named
184              
185             ./Build migration --named create_users
186              
187             A string to use when creating a new migration file. The above command would
188             create a file named F.
189              
190             =cut
191              
192             __PACKAGE__->add_property( context => 'test' );
193             __PACKAGE__->add_property( replace_config => undef );
194             __PACKAGE__->add_property( db_config_key => 'dbi' );
195             __PACKAGE__->add_property( db_client => undef );
196             __PACKAGE__->add_property( drop_db => 0 );
197             __PACKAGE__->add_property( db_super_user => undef );
198             __PACKAGE__->add_property( db_super_pass => undef );
199             __PACKAGE__->add_property( test_env => {} );
200             __PACKAGE__->add_property( meta_table => 'metadata' );
201             __PACKAGE__->add_property( named => undef );
202              
203             ##############################################################################
204              
205             =head2 Actions
206              
207             =head3 test
208              
209             =begin comment
210              
211             =head3 ACTION_test
212              
213             =end comment
214              
215             Overrides the default implementation to ensure that tests are only run in the
216             "test" context, to make sure that the database is up-to-date, and to set up
217             the test environment with values stored in C.
218              
219             =cut
220              
221             sub ACTION_test {
222 0     0 1 0 my $self = shift;
223 0 0       0 die qq{ERROR: Tests can only be run in the "test" context\n}
224             . "Try `./Build test --context test`\n"
225             unless $self->context eq 'test';
226              
227             # Make sure the database is up-to-date.
228 0         0 $self->depends_on('db');
229              
230             # Tell the tests where to find stuff, like pgTAP.
231 0         0 local %ENV = ( %ENV, %{ $self->test_env } );
  0         0  
232              
233             # Make it so.
234 0         0 $self->SUPER::ACTION_test(@_);
235             }
236              
237             ##############################################################################
238              
239             =head3 migration
240              
241             =begin comment
242              
243             =head3 ACTION_migration
244              
245             =end comment
246              
247             Creates a new migration script in the F directory. Best used in
248             combination with the C<--named> option.
249              
250             =cut
251              
252             sub ACTION_migration {
253 0     0 1 0 my $self = shift;
254 0         0 File::Path::mkpath('sql');
255 0 0       0 die "Can't create directory sql: $!" unless -d 'sql';
256 0   0     0 my $file = File::Spec->catfile(
257             'sql',
258             (time . '-' . $self->named || 'migration') . '.sql'
259             );
260 0 0       0 my $fh = IO::File->new("> $file") or die "Can't create $file: $!";
261 0         0 print $fh "-- $file SQL Migration\n\n";
262 0         0 close $fh;
263 0         0 return $self;
264             }
265              
266             ##############################################################################
267              
268             =head3 config_data
269              
270             =begin comment
271              
272             =head3 ACTION_config_data
273              
274             =end comment
275              
276             Overrides the default implementation to completely change its behavior. :-)
277             Rather than creating a whole new configuration file in Module::Build's weird
278             way, this action now simply opens the application file (that returned by
279             C and replaces all text matching C with the
280             configuration file for the current context. This means that an installed app
281             is effectively configured for the proper context at installation time.
282              
283             =cut
284              
285             sub ACTION_config_data {
286 0     0 1 0 my $self = shift;
287 0 0       0 my $replace = $self->replace_config or return $self;
288              
289 0         0 my $file = File::Spec->catfile( split qr{/}, $self->dist_version_from);
290 0         0 my $blib = File::Spec->catfile( $self->blib, $file );
291              
292             # Die if there is no file
293 0 0       0 die qq{ERROR: "$blib" seems to be missing!\n} unless -e $blib;
294              
295             # Make sure we have a config file.
296 0         0 $self->cx_config;
297              
298             # Figure out where we're going to install this beast.
299 0         0 $file .= '.new';
300 0         0 my $new = File::Spec->catfile( $self->blib, $file );
301 0         0 my $config = $self->cx_config_file;
302 0 0       0 $replace = quotemeta $replace unless ref $replace eq 'Regexp';
303              
304             # Update the file.
305 0 0       0 open my $orig, '<', $blib or die qq{Cannot open "$blib": $!\n};
306 0 0       0 open my $temp, '>', $new or die qq{Cannot open "$new": $!\n};
307 0         0 while (<$orig>) {
308 0         0 s/$replace/$config/g;
309 0         0 print $temp $_;
310             }
311 0         0 close $orig;
312 0         0 close $temp;
313              
314             # Make the switch.
315 0 0       0 rename $new, $blib or die "Cannot rename '$blib' to '$new': $!\n";
316 0 0       0 my $mode = oct(444) | ( $self->is_executable($blib) ? oct(111) : 0 );
317 0         0 chmod $mode, $blib;
318 0         0 return $self;
319             }
320              
321             ##############################################################################
322              
323             =head3 db
324              
325             =begin comment
326              
327             =head3 ACTION_db
328              
329             =end comment
330              
331             This action creates or updates the database for the current context. If
332             C is set to a true value, the database will be dropped and created
333             anew. Otherwise, if the database already exists, it will be brought up-to-date
334             from the files in the F directory.
335              
336             Those files are expected to all be SQL scripts. They must all start with a
337             number followed by a dash. The number indicates the order in which the scripts
338             should be run. For example, you might have SQL files like so:
339              
340             sql/001-types.sql
341             sql/002-tables.sql
342             sql/003-triggers.sql
343             sql/004-functions.sql
344             sql/005-indexes.sql
345              
346             The SQL files will be run in integer order to build or update the database.
347             Module::Build::DB will track the current schema update number corresponding to
348             the last run SQL script in the C table in the database.
349              
350             If any of the scripts has an error, Module::Build::DB will immediately exit with
351             the relevant error. To prevent half-way applied updates, the SQL scripts
352             should use transactions as appropriate.
353              
354             =cut
355              
356             sub ACTION_db {
357 0     0 1 0 my $self = shift;
358              
359             # Get the database configuration information.
360 0         0 my $config = $self->cx_config;
361              
362 0         0 my ( $db, $cmd ) = $self->db_cmd( $config->{$self->db_config_key} );
363              
364             # Does the database exist?
365 0 0       0 my $db_exists = $self->drop_db ? 1 : $self->_probe(
366             $self->{driver}->get_check_db_command($cmd, $db)
367             );
368              
369 0 0       0 if ( $db_exists ) {
370             # Drop the existing database?
371 0 0       0 if ( $self->drop_db ) {
372 0         0 $self->log_info(qq{Dropping the "$db" database\n});
373 0 0       0 $self->do_system(
374             $self->{driver}->get_drop_db_command($cmd, $db)
375             ) or die;
376             } else {
377             # Just run the upgrades and be done with it.
378 0         0 $self->upgrade_db( $db, $cmd );
379 0         0 return;
380             }
381             }
382              
383             # Now create the database and run all of the SQL files.
384 0         0 $self->log_info(qq{Creating the "$db" database\n});
385 0 0       0 $self->do_system( $self->{driver}->get_create_db_command($cmd, $db) ) or die;
386              
387             # Add the metadata table and run all of the schema scripts.
388 0         0 $self->create_meta_table( $db, $cmd );
389 0         0 $self->upgrade_db( $db, $cmd );
390             }
391              
392             ##############################################################################
393              
394             =head2 Instance Methods
395              
396             =head3 cx_config
397              
398             my $config = $build->cx_config;
399              
400             Uses L to read and return the contents of the current
401             context's configuration file.
402              
403             =cut
404              
405             sub cx_config {
406 2     2 1 4 my $self = shift;
407 2 100       47 return $self->{cx_config} if $self->{cx_config};
408 2         38 my @stems = map {
409 1         3 File::Spec->catfile( $_ => $self->context )
410             } qw(conf etc);
411 1         912 require Config::Any;
412 1         7674 my $cfg = Config::Any->load_stems({ stems => \@stems, use_ext => 1 })->[0];
413 1         23759 my ($file, $config) = %{ $cfg };
  1         6  
414 1         13 $self->cx_config_file($file);
415 1         9 return $self->{cx_config} = $config;
416             }
417              
418             =head3 cx_config_file
419              
420             my $config_file = $build->cx_config_file;
421              
422             Returns the name of the context configuration file loaded by C. If
423             C has not yet been called and loaded a file, it will be.
424              
425             =cut
426              
427             sub cx_config_file {
428 2     2 1 88374 my $self = shift;
429 2 100       25 return $self->{cx_config_file} = shift if @_;
430 1         12 $self->cx_config; # Make sure we've found the file.
431 1         20 return $self->{cx_config_file};
432             }
433              
434             =head3 db_cmd
435              
436             my ($db_name, $db_cmd) = $build->db_cmd($db_config);
437              
438             Uses the current context's configuration to determine all of the options to
439             run the C for building the database. Returns the name of the
440             database and an array ref representing the C command and all of its
441             options, suitable for passing to C. The database name is not included
442             so as to enable connecting to another database (e.g., template1 on PostgreSQL)
443             to create the database.
444              
445             =cut
446              
447             sub db_cmd {
448 0     0 1   my ($self, $dconf) = @_;
449 0 0 0       return @{$self}{qw(db_name db_cmd)} if $self->{db_cmd} && $self->{db_name};
  0            
450              
451 0           require DBI;
452 0           my (undef, $driver, undef, undef, $driver_dsn) = DBI->parse_dsn($dconf->{dsn});
453 0           my %dsn = map { split /=/ } split /;/, $driver_dsn;
  0            
454              
455 0           $driver = __PACKAGE__ . "D::$driver";
456 0 0 0       eval "require $driver"
457             or die $@ || "Package $driver did not return a true value\n";
458              
459             # Make sure we have a client.
460 0 0         $self->db_client( $driver->get_client ) unless $self->db_client;
461              
462 0           my ($db, $cmd) = $driver->get_db_and_command($self->db_client, {
463 0           %{ $dconf },
464             %dsn,
465             db_super_user => $self->db_super_user,
466             db_super_pass => $self->db_super_pass,
467             });
468              
469 0           $self->{db_cmd} = $cmd;
470 0           $self->{db_name} = $db;
471 0           $self->{driver} = $driver;
472 0           return ($db, $cmd);
473             }
474              
475             ##############################################################################
476              
477             =head3 create_meta_table
478              
479             my ($db_name, $db_cmd ) = $build->db_cmd;
480             $build->create_meta_table( $db_name, $db_cmd );
481              
482             Creates the C table, which Module::Build::DB uses to track the current
483             schema version (corresponding to update numbers on the SQL scripts in F
484             and other application metadata. If the table already exists, it will be
485             dropped and recreated. One row is initially inserted, setting the
486             "schema_version" to 0.
487              
488             =cut
489              
490             sub create_meta_table {
491 0     0 1   my ($self, $db, $cmd) = @_;
492 0           my $quiet = $self->quiet;
493 0 0         $self->quiet(1) unless $quiet;
494 0           my $driver = $self->{driver};
495 0 0         $self->do_system($driver->get_execute_command(
496             $cmd, $db,
497             $driver->get_meta_table_sql($self->meta_table),
498             )) or die;
499 0           my $table = $self->meta_table;
500 0 0         $self->do_system( $driver->get_execute_command($cmd, $db, qq{
501             INSERT INTO $table VALUES ( 'schema_version', 0, '' );
502             })) or die;
503 0 0         $self->quiet(0) unless $quiet;
504             }
505              
506             ##############################################################################
507              
508             =head3 upgrade_db
509              
510             my ($db_name, $db_cmd ) = $build->db_cmd;
511             push $db_cmd, '--dbname', $db_name;
512             $self->upgrade_db( $db_name, $db_cmd );
513              
514             Upgrades the database using all of the schema files in the F directory,
515             applying each in numeric order, setting the schema version upon the success of
516             each, and exiting upon any error.
517              
518             =cut
519              
520             sub upgrade_db {
521 0     0 1   my ($self, $db, $cmd) = @_;
522              
523 0           $self->log_info(qq{Updating the "$db" database\n});
524 0           my $driver = $self->{driver};
525 0           my $table = $self->meta_table;
526              
527             # Get the current version number of the schema.
528 0           my $curr_version = $self->_probe(
529             $driver->get_execute_command(
530             $cmd, $db,
531             qq{SELECT value FROM $table WHERE label = 'schema_version'},
532             )
533             );
534              
535 0           my $quiet = $self->quiet;
536             # Apply all relevant upgrade files.
537 0           for my $sql (sort grep { -f } glob 'sql/[0-9]*-*.sql' ) {
  0            
538             # Compare upgrade version numbers.
539 0           ( my $new_version = $sql ) =~ s{^sql[/\\](\d+)-.+}{$1};
540 0 0         next unless $new_version > $curr_version;
541              
542             # Apply the version.
543 0 0         $self->do_system( $driver->get_file_command($cmd, $db, $sql) ) or die;
544 0 0         $self->quiet(1) unless $quiet;
545 0 0         $self->do_system( $driver->get_execute_command($cmd, $db, qq{
546             UPDATE $table
547             SET value = $new_version
548             WHERE label = 'schema_version'
549             })) or die;
550 0 0         $self->quiet(0) unless $quiet;
551             }
552             }
553              
554             sub _probe {
555 0     0     my $self = shift;
556 0           my $ret = $self->_backticks(@_);
557 0           chomp $ret;
558 0           return $ret;
559             }
560              
561             1;
562              
563             __END__