File Coverage

blib/lib/Database/Migrator/Core.pm
Criterion Covered Total %
statement 33 89 37.0
branch 0 16 0.0
condition 0 9 0.0
subroutine 11 21 52.3
pod 0 2 0.0
total 44 137 32.1


line stmt bran cond sub pod time code
1             package Database::Migrator::Core;
2              
3 1     1   973 use strict;
  1         2  
  1         29  
4 1     1   4 use warnings;
  1         1  
  1         30  
5 1     1   4 use namespace::autoclean;
  1         1  
  1         11  
6              
7             our $VERSION = '0.12';
8              
9 1     1   384 use Database::Migrator::Types qw( ArrayRef Bool Dir File Maybe Str );
  1         3  
  1         6  
10 1     1   5944 use DBI;
  1         13259  
  1         62  
11 1     1   9 use Eval::Closure qw( eval_closure );
  1         1  
  1         49  
12 1     1   682 use Log::Dispatch;
  1         199383  
  1         40  
13 1     1   9 use Moose::Util::TypeConstraints qw( duck_type );
  1         3  
  1         16  
14 1     1   475 use MooseX::Getopt::OptionTypeMap;
  1         2  
  1         34  
15 1     1   6 use Try::Tiny;
  1         1  
  1         62  
16              
17 1     1   6 use Moose::Role;
  1         2  
  1         8  
18              
19             with 'MooseX::Getopt::Dashes';
20              
21             MooseX::Getopt::OptionTypeMap->add_option_type_to_map(
22             Maybe [Str] => '=s',
23             );
24              
25             requires qw(
26             _create_database
27             _driver_name
28             _drop_database
29             _run_ddl
30             );
31              
32             has database => (
33             is => 'ro',
34             isa => Str,
35             required => 1,
36             );
37              
38             has [qw( username password host port )] => (
39             is => 'ro',
40             isa => Maybe [Str],
41             default => undef,
42             );
43              
44             has migration_table => (
45             is => 'ro',
46             isa => Str,
47             required => 1,
48             );
49              
50             has migrations_dir => (
51             is => 'ro',
52             isa => Dir,
53             coerce => 1,
54             required => 1,
55             );
56              
57             has schema_file => (
58             is => 'ro',
59             isa => File,
60             coerce => 1,
61             required => 1,
62             );
63              
64             has _database_exists => (
65             is => 'ro',
66             isa => Bool,
67             init_arg => undef,
68             lazy => 1,
69             builder => '_build_database_exists',
70             );
71              
72             has __pending_migrations => (
73             traits => ['Array'],
74             is => 'ro',
75             isa => ArrayRef [Dir],
76             init_arg => undef,
77             lazy => 1,
78             builder => '_build_pending_migrations',
79             handles => {
80             _pending_migrations => 'elements',
81             has_pending_migrations => 'count',
82             },
83             );
84              
85             has dbh => (
86             traits => ['NoGetopt'],
87             is => 'ro',
88             isa => 'DBI::db',
89             init_arg => undef,
90             lazy => 1,
91             builder => '_build_dbh',
92             );
93              
94             has logger => (
95             traits => ['NoGetopt'],
96             is => 'ro',
97             isa => duck_type( [qw( debug info )] ),
98             lazy => 1,
99             builder => '_build_logger',
100             );
101              
102             has verbose => (
103             is => 'ro',
104             isa => Bool,
105             default => 0,
106             );
107              
108             has quiet => (
109             is => 'ro',
110             isa => Bool,
111             default => 0,
112             );
113              
114             has dry_run => (
115             is => 'ro',
116             isa => Bool,
117             default => 0,
118             );
119              
120             around BUILDARGS => sub {
121             my $orig = shift;
122             my $class = shift;
123              
124             my $p = $class->$orig(@_);
125              
126             $p->{username} = delete $p->{user}
127             if exists $p->{user};
128              
129             return $p;
130             };
131              
132       0 0   sub BUILD { }
133             after BUILD => sub {
134             my $self = shift;
135              
136             die 'Cannot be both quiet and verbose'
137             if $self->quiet() && $self->verbose();
138             };
139              
140             sub create_or_update_database {
141 0     0 0   my $self = shift;
142              
143 0 0         if ( $self->_database_exists() ) {
144 0           my $database = $self->database();
145 0           $self->logger()->debug("The $database database already exists");
146             }
147             else {
148 0           $self->_create_database();
149 0           $self->_run_ddl( $self->schema_file() );
150             }
151              
152 0           $self->_run_migrations();
153              
154 0           return;
155             }
156              
157             sub _run_migrations {
158 0     0     my $self = shift;
159              
160 0           $self->_run_one_migration($_) for $self->_pending_migrations();
161             }
162              
163             sub _run_one_migration {
164 0     0     my $self = shift;
165 0           my $migration = shift;
166              
167 0           my $name = $migration->basename();
168              
169 0           $self->logger()->info("Running migration - $name");
170              
171 0           my @files = grep { !$_->is_dir() } $migration->children( no_hidden => 1 );
  0            
172              
173 0           for my $file ( sort _numeric_or_alpha_sort @files ) {
174 0           my $basename = $file->basename();
175 0 0         if ( $file =~ /\.sql/ ) {
176 0           $self->logger()->debug(" - running $basename as sql");
177 0           $self->_run_ddl($file);
178             }
179             else {
180 0           $self->logger()->debug(" - running $basename as perl code");
181              
182 0           my $perl = $file->slurp();
183              
184 0           my $sub = eval_closure( source => $perl );
185              
186 0 0         next if $self->dry_run();
187              
188 0           $sub->($self);
189             }
190             }
191              
192 0 0         return if $self->dry_run();
193              
194 0           my $table = $self->dbh()->quote_identifier( $self->migration_table() );
195 0           $self->dbh()
196             ->do( "INSERT INTO $table (migration) VALUES (?)", undef, $name );
197              
198 0           return;
199             }
200              
201             sub _build_pending_migrations {
202 0     0     my $self = shift;
203              
204 0           my $table = $self->migration_table();
205              
206 0           my %ran;
207 0 0         if ( grep { $_ =~ /\b\Q$table\E\b/ } $self->dbh()->tables() ) {
  0            
208 0           my $quoted = $self->dbh()->quote_identifier($table);
209              
210             %ran
211 0           = map { $_ => 1 }
212 0 0         @{ $self->dbh()
  0            
213             ->selectcol_arrayref("SELECT migration FROM $quoted") || [] };
214             }
215              
216             return [
217 0           sort _numeric_or_alpha_sort grep { !$ran{ $_->basename() } }
218 0           grep { $_->is_dir() }
  0            
219             $self->migrations_dir()->children( no_hidden => 1 )
220             ];
221             }
222              
223             sub _build_logger {
224 0     0     my $self = shift;
225              
226 0 0         my $outputs
    0          
227             = $self->quiet()
228             ? [ 'Null', min_level => 'emerg' ]
229             : [
230             'Screen',
231             min_level => ( $self->verbose() ? 'debug' : 'info' ),
232             newline => 1,
233             ];
234              
235 0           return Log::Dispatch->new( outputs => [$outputs] );
236             }
237              
238             sub _build_database_exists {
239 0     0     my $self = shift;
240              
241             ## no critic (RequireBlockTermination)
242 0   0 0     return try { $self->_build_dbh(); 1 } || 0;
  0            
  0            
243             }
244              
245             sub _build_dbh {
246 0     0     my $self = shift;
247              
248 0           return DBI->connect(
249             'dbi:' . $self->_driver_name() . ':database=' . $self->database(),
250             $self->username(),
251             $self->password(), {
252             RaiseError => 1,
253             PrintError => 1,
254             PrintWarn => 1,
255             ShowErrorStatement => 1,
256             },
257             );
258             }
259              
260             sub _numeric_or_alpha_sort {
261 0     0     my ( $a_num, $a_alpha ) = $a->basename() =~ /^(\d+)(.+)/;
262 0           my ( $b_num, $b_alpha ) = $b->basename() =~ /^(\d+)(.+)/;
263              
264 0   0       $a_num ||= 0;
265 0   0       $b_num ||= 0;
266              
267 0   0       return ( $a_num <=> $b_num or $a_alpha cmp $b_alpha );
268             }
269              
270             1;
271              
272             # ABSTRACT: Core role for Database::Migrator implementation classes
273              
274             __END__
275              
276             =pod
277              
278             =encoding UTF-8
279              
280             =head1 NAME
281              
282             Database::Migrator::Core - Core role for Database::Migrator implementation classes
283              
284             =head1 VERSION
285              
286             version 0.12
287              
288             =head1 SYNOPSIS
289              
290             package Database::Migrator::SomeDB;
291              
292             use Moose;
293             with 'Database::Migrator::Core';
294              
295             sub _build_database_exists { }
296             sub _build_dbh { }
297             sub _create_database { }
298              
299             =head1 DESCRIPTION
300              
301             This role implements the bulk of the migration logic, leaving a few details up
302             to DBMS-specific classes.
303              
304             You can then subclass these DBMS-specific classes to provide defaults for
305             various attributes, or to override some of the implementation.
306              
307             =for Pod::Coverage BUILD
308             create_or_update_database
309              
310             =head1 PUBLIC ATTRIBUTES
311              
312             This role defines the following public attributes. These attributes may be
313             provided via the command line or you can set defaults for them in a subclass.
314              
315             =over 4
316              
317             =item * database
318              
319             The name of the database that will be created or migrated. This is required.
320              
321             =item * username, password, host, port
322              
323             These parameters are used when connecting to the database. They are all
324             optional.
325              
326             =item * migration_table
327              
328             The name of the table which stores the name of applied migrations. This is
329             required.
330              
331             =item * migrations_dir
332              
333             The directory containing migrations. This is required, but it is okay if the
334             directory is empty.
335              
336             =item * schema_file
337              
338             The full path to the file containing the initial schema for the database. This
339             will be used to create the database if it doesn't already exist. This is required.
340              
341             =item * verbose
342              
343             This affects the verbosity of output logging. Defaults to false.
344              
345             =item * quiet
346              
347             If this is true, then no output will logged at all. Defaults to false.
348              
349             =item * dry_run
350              
351             If this is true, no migrations are actually run. Instead, the code just logs
352             what it I<would> do. Defaults to false.
353              
354             =back
355              
356             =head1 METHODS
357              
358             This role provide just one public method, C<create_or_update_database()>.
359              
360             It will create a new database if none exists.
361              
362             It will run all unapplied migrations on this schema once it does exist.
363              
364             =head1 REQUIRED METHODS
365              
366             If you want to create your own implementation class, you must implement the
367             following methods. All of these methods should throw an error
368              
369             =head2 $migration->_create_database()
370              
371             This should create an I<empty> database. This role will take care of executing
372             the DDL for defining the schema.
373              
374             =head2 $migration->_driver_name()
375              
376             This return a string containing the DBI driver name, such as "mysql" or "Pg".
377              
378             =head2 $migration->_drop_database()
379              
380             This should drop the database. Right now it is only used for testing.
381              
382             =head2 $migration->_run_ddl($ddl)
383              
384             Given a string containing one or more DDL statements, this method must run
385             that DDL against the database.
386              
387             =head1 OVERRIDEABLE ATTRIBUTES AND METHODS
388              
389             There are a number of attributes methods in this role that you may wish to
390             override in a custom subclass of an implementation.
391              
392             For any attribute where you provide a default value, make sure to also set C<<
393             required => 0 >> as well.
394              
395             =over 4
396              
397             =item * database attribute
398              
399             You can provide a default database name.
400              
401             =item * username, password, host, and port attributes
402              
403             You can provide a default values for these connection attributes.
404              
405             =item * migration_table
406              
407             You can provide a default table name.
408              
409             =item * migrations_dir
410              
411             You can provide a default directory.
412              
413             =item * schema_file
414              
415             You can provide a default file name.
416              
417             =item * _build_logger()
418              
419             You must return an object with C<debug()> and C<info()> methods.
420              
421             =back
422              
423             =head1 SUPPORT
424              
425             Bugs may be submitted through L<https://github.com/maxmind/Database-Migrator/issues>.
426              
427             =head1 AUTHOR
428              
429             Dave Rolsky <autarch@urth.org>
430              
431             =head1 COPYRIGHT AND LICENSE
432              
433             This software is Copyright (c) 2012 - 2017 by MaxMind, Inc.
434              
435             This is free software, licensed under:
436              
437             The Artistic License 2.0 (GPL Compatible)
438              
439             =cut