The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

Name

Module::Build::DB - Build, configure, and test database-backed applications

Synopsis

In Build.PL:

  use strict;
  use Module::Build::DB;

  Module::Build::DB->new(
      module_name   => 'MyApp',
      db_config_key => 'dbi',
      context       => 'test',
  )->create_build_script;

On the command-line:

  perl Build.PL
  ./Build --db_super_user postgres
  ./Build db --context test
  ./Build test

Description

This module subclasses Module::Build to provide added functionality for configuring, building, and testing database-backed applications. It uses a simple Rails-style numbered migration scheme, although migration scripts are written in pure SQL, not Perl.

Frankly, this isn't a great module. Some reasons:

  • The numbered method of tracking migration dependencies has very little flexibility.

  • Subclassing Module::Build is a really bad way to extend the build system, because you can't really mix in other build features.

Someday, I hope to fix the first issue by looking more closely at database change management, and perhaps by adopting a completely different approach. The latter problem I would likely solve by completely separating the migration code from the build system, and then integrating as appropriate (hopefully Module::Build will get proper plugins someday).

But in the meantime, I have working code that depends on this simple implementation (which does support PostgreSQL, SQLite and MySQL), and I want it to be easy for people to get at this dependency. So here we are.

Class Interface

Properties

Module::Build::DB defines these properties in addition to those specified by Module::Build. Note that these may be specified either in Build.PL or on the command-line.

context

  perl Build.PL --context test

Specifies the context in which the build will run. The context associates the build with a configuration file, and therefore must be named for a configuration file your project. For example, to build in the "dev" context, there must be a dev.yml file (or dev.json or some other format supported by Config::Any) in the conf/ or etc/ directory of your project. Defaults to "test", which is also the only required context.

db_client

  perl Build.PL --db_client /usr/local/pgsql/bin/pgsql

Specifies the location of the database command-line client. Defaults to psql, mysql, or sqlite3, depending on the value of the DSN in the context configuration file.

drop_db

  ./Build db --drop_db 1

Tells the "db" action to drop the database and build a new one. When this property is set to a false value (the default), an existing database for the current context will not be dropped, but it will be brought up-to-date by ./Build db.

db_config_key

The config key under which DBI configuration is stored in the configuration file. Defaults to "dbi". The keys that should be under this configuration key are:

  • dsn

  • username

  • password

db_super_user

db_super_pass

  perl Build.PL --db_super_user root --db_super_pass s3cr1t

Specifies a super user and password to be used to connect to the database. This is important if you need to use a different database user to create and update the database than to run your app. Most likely you'll use this for production deployments. If not specified the user name and password from the the context configuration file will be used.

test_env

  ./Build db --test_env CATALYST_DEBUG=0 CATALYST_CONFIG=conf/test.json

Optional hash reference of environment variables to set for the lifetime of ./Build test. This can be useful for making Catalyst less verbose, for example. Another use is to tell PostgreSQL where to find pgTAP functions when they're installed in a schema outside the normal search path in your database:

  ./Build db --test_env PGOPTIONS='--search_path=tap,public'

meta_table

  ./Build db --meta_table mymeta

The name of the metadata table that Module::Build::DB uses to track migrations in the database. Defaults to "metadata". Change if that name conflicts with other objects in your application's database, but use only characters that don't require quoting in the database (e.g., "my_meta" but not "my meta").

replace_config

  Module::Build::DB->new(
      module_name    => 'MyApp',
      db_config_key  => 'dbi',
      replace_config => 'conf/dev.json',
  )->create_build_script;

Set to a string or regular expression (using qr//) and, the module_name file will be opened during ./Build and matching strings replaced with name of the context configuration file. This is useful when deploying Catalyst applications, for example, where your module_name file might have something like this in it:

  __PACKAGE__->config(
      name                   => 'MyApp',
      'Plugin::ConfigLoader' => { file => 'conf/dev.json' },
  );

The conf/dev.json string would be replaced in the copy of the file in blib/lib with the context configuration file name. Use a regular expression if you want to cover a variety of values, as in:

      replace_config => qr{etc/[^.].json},

named

  ./Build migration --named create_users

A string to use when creating a new migration file. The above command would create a file named sql/$time-create_users.sql.

Actions

test

Overrides the default implementation to ensure that tests are only run in the "test" context, to make sure that the database is up-to-date, and to set up the test environment with values stored in test_env.

migration

Creates a new migration script in the sql directory. Best used in combination with the --named option.

config_data

Overrides the default implementation to completely change its behavior. :-) Rather than creating a whole new configuration file in Module::Build's weird way, this action now simply opens the application file (that returned by dist_version_from and replaces all text matching replace_config with the configuration file for the current context. This means that an installed app is effectively configured for the proper context at installation time.

db

This action creates or updates the database for the current context. If drop_db is set to a true value, the database will be dropped and created anew. Otherwise, if the database already exists, it will be brought up-to-date from the files in the sql directory.

Those files are expected to all be SQL scripts. They must all start with a number followed by a dash. The number indicates the order in which the scripts should be run. For example, you might have SQL files like so:

  sql/001-types.sql
  sql/002-tables.sql
  sql/003-triggers.sql
  sql/004-functions.sql
  sql/005-indexes.sql

The SQL files will be run in integer order to build or update the database. Module::Build::DB will track the current schema update number corresponding to the last run SQL script in the metadata table in the database.

If any of the scripts has an error, Module::Build::DB will immediately exit with the relevant error. To prevent half-way applied updates, the SQL scripts should use transactions as appropriate.

Instance Methods

cx_config

  my $config = $build->cx_config;

Uses Config::Any to read and return the contents of the current context's configuration file.

cx_config_file

  my $config_file = $build->cx_config_file;

Returns the name of the context configuration file loaded by cx_config. If cx_config has not yet been called and loaded a file, it will be.

db_cmd

  my ($db_name, $db_cmd) = $build->db_cmd($db_config);

Uses the current context's configuration to determine all of the options to run the db_client for building the database. Returns the name of the database and an array ref representing the db_client command and all of its options, suitable for passing to system. The database name is not included so as to enable connecting to another database (e.g., template1 on PostgreSQL) to create the database.

create_meta_table

  my ($db_name, $db_cmd ) = $build->db_cmd;
  $build->create_meta_table( $db_name, $db_cmd );

Creates the metadata table, which Module::Build::DB uses to track the current schema version (corresponding to update numbers on the SQL scripts in sql and other application metadata. If the table already exists, it will be dropped and recreated. One row is initially inserted, setting the "schema_version" to 0.

upgrade_db

  my ($db_name, $db_cmd ) = $build->db_cmd;
  push $db_cmd, '--dbname', $db_name;
  $self->upgrade_db( $db_name, $db_cmd );

Upgrades the database using all of the schema files in the sql directory, applying each in numeric order, setting the schema version upon the success of each, and exiting upon any error.

Author

David E. Wheeler <david@justatheory.com>

Copyright

Copyright (c) 2008-2010 David E. Wheeler. Some Rights Reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.