File Coverage

blib/lib/App/PGMultiDeploy.pm
Criterion Covered Total %
statement 26 67 38.8
branch 0 10 0.0
condition n/a
subroutine 9 16 56.2
pod 1 1 100.0
total 36 94 38.3


line stmt bran cond sub pod time code
1              
2 1     1   14064 use 5.006;
  1         4  
3 1     1   5 use strict;
  1         1  
  1         18  
4 1     1   4 use warnings;
  1         8  
  1         32  
5             package App::PGMultiDeploy;
6 1     1   425 use Moo;
  1         9615  
  1         4  
7 1     1   2330 use DBI;
  1         12496  
  1         57  
8 1     1   9 use Carp;
  1         2  
  1         52  
9 1     1   708 use Config::IniFiles;
  1         23810  
  1         38  
10 1     1   461 use PGObject::Util::DBChange;
  1         22449  
  1         32  
11 1     1   507 use Try::Tiny;
  1         998  
  1         591  
12              
13             =head1 NAME
14              
15             App::PGMultiDeploy - OO deployment to multiple dbs for Pg
16              
17             =head1 VERSION
18              
19             Version 0.004.001
20              
21             =cut
22              
23             our $VERSION = 0.004001;
24              
25              
26             =head1 SYNOPSIS
27              
28             This package provides a library and a command line utility to run sql scripts
29             on multiple pg databases relying on two phase commit to ensure the script
30             succeeds or fails. Scripts can only be applied once and the intended use is to
31             manage schema changes over time in databases subject to row-level logical
32             replication.
33              
34             Features:
35              
36             =over
37              
38             =item Recovery for partial application
39              
40             A change file is not re-applied if it has been applied before unless the file
41             has changed. This means if another system using PGObject::Util::DBChange
42             applies a file to one db, you can still safely use it here.
43              
44             =item Two phase commit
45              
46             A change file either commits or rolls back on every database in a group
47              
48             =item Reuse of libpq tooling
49              
50             .pgpass etc files work with this tool
51              
52             =item Logging of failures in separate transaction
53              
54             =back
55              
56             Use as a library:
57              
58             use App::PGMultiDeploy;
59              
60             my $foo = App::PGMultiDeploy->new( config_file => 'path/to/conf.ini',
61             change_file => 'path/to/change.sql',
62             dbgroup => 'defined_in_config');
63             $foo->deploy;
64              
65             use as a commandline:
66              
67             pg_multideploy --config=/path/to/conf.ini --sql=mychanges.sql --dbgroup=foo
68              
69             =head1 PROPERTIES
70              
71             =head2 config_file (--config)
72              
73             The ini file defining the environment configuration
74              
75             =cut
76              
77             has config_file => (is => 'ro',
78             isa => sub { die 'Config File not found' unless -f $_[0] }
79             );
80              
81             =head2 config (lazily loaded from config file)
82              
83             =cut
84              
85             =head2 dbgroup (--dbgroup)
86              
87             =cut
88              
89             has dbgroup => (is => 'ro');
90              
91             =head2 change_file (--sql)
92              
93             Path to db change
94              
95             =cut
96              
97             has change_file => (is => 'ro',
98             isa => sub { die 'Change File not found' unless -f $_[0] }
99             );
100              
101             =head2 config
102              
103             The configuration object loaded from the config file
104              
105             =cut
106              
107             has config => (is => 'lazy');
108              
109             sub _build_config{
110 0     0     my ($self) = @_;
111 0           my $config = Config::IniFiles->new(-file => $self->config_file);
112 0           return $config;
113             }
114             =head2 dbchange
115              
116             The db change object, loaded from file
117              
118             =cut
119              
120             has dbchange => (is => 'lazy');
121              
122             sub _build_dbchange{
123 0     0     my ($self) = @_;
124 0           my $dbchange = PGObject::Util::DBChange->new(
125             path => $self->change_file,
126             commit_txn => "FAIL" # don't allow direct application
127             );
128             }
129              
130             has succeeded => (is => 'rwp', default => 1);
131              
132             =head1 SUBROUTINES/METHODS
133              
134             =head2 deploy
135              
136             =cut
137              
138             sub deploy {
139 0     0 1   my ($self) = @_;
140 0           local $PGObject::Util::DBChange::commit = 0;
141 0 0         my @dbgroup = $self->config->val("dbgroups", $self->dbgroup)
142             or die 'Cannot find db group ' . $self->dbgroup;
143            
144 0           my @dbs = map { DBI->connect($self->_connstr($_), undef, undef,
  0            
145             {AutoCommit => 1, pg_server_prepare => 0}) }
146             @dbgroup;
147 0           for (@dbs) {
148 0 0         PGObject::Util::DBChange::init($_)
149             if PGObject::Util::DBChange::needs_init($_);
150             }
151 0           $_->commit for @dbs;
152 0           my @logs = map { $self->_apply_if_needed($_) } @dbs;
  0            
153 0 0         if ($self->succeeded){
154             $_->{dbh}->do("COMMIT PREPARED '$_->{txn_id}'")
155 0           for grep {defined $_} @logs
  0            
156             } else {
157             $_->{dbh}->do("ROLLBACK PREPARED '$_->{txn_id}'")
158 0           for grep {defined $_} @logs
  0            
159             }
160 0           $self->dbchange->log(%$_) for grep {defined $_} @logs;
  0            
161 0           $_->commit for @dbs;
162             }
163              
164             sub _connstr{
165 0     0     my ($self, $dbname) = @_;
166 0           my $cnx = $self->config->val('databases', $dbname);
167 0 0         die 'No connection configured for ' . $dbname unless defined $cnx;
168 0           warn $cnx;
169 0           return 'dbi:Pg:' . $self->config->val('databases', $dbname);
170             }
171              
172             my $counter = 0;
173             sub _apply_if_needed {
174 0     0     my ($self, $dbh) = @_;
175 0           ++$counter;
176 0           my $txn_id = "multideploy $counter";
177             my $dbchange = PGObject::Util::DBChange->new(
178 0           %{$self->dbchange},
  0            
179             commit_txn => "PREPARE TRANSACTION '$txn_id';",
180             );
181 0 0         if ($dbchange->is_applied($dbh)){
182 0           warn 'Change already applied';
183 0           return;
184             } else {
185             try {
186 0     0     $dbchange->apply($dbh);
187             } catch {
188 0     0     warn "Could not apply change";
189 0           $self->_set_succeeded(0);
190 0           };
191 0           return {state => $DBI::state, errstr => $DBI::errstr, dbh => $dbh,
192             txn_id => $txn_id };
193             }
194             }
195              
196             =head1 AUTHOR
197              
198             Chris Travers, C<< >>
199              
200             =head1 BUGS
201              
202             Please report any bugs or feature requests to C, or through
203             the web interface at L. I will be notified, and then you'll
204             automatically be notified of progress on your bug as I make changes.
205              
206              
207              
208              
209             =head1 SUPPORT
210              
211             You can find documentation for this module with the perldoc command.
212              
213             perldoc App::PGMultiDeploy
214              
215              
216             You can also look for information at:
217              
218             =over 4
219              
220             =item * RT: CPAN's request tracker (report bugs here)
221              
222             L
223              
224             =item * AnnoCPAN: Annotated CPAN documentation
225              
226             L
227              
228             =item * CPAN Ratings
229              
230             L
231              
232             =item * Search CPAN
233              
234             L
235              
236             =back
237              
238              
239             =head1 ACKNOWLEDGEMENTS
240              
241             Many thanks to Sedex Global for funding the initial version of this tool.
242              
243             =head1 LICENSE AND COPYRIGHT
244              
245             Copyright 2016-2017 Chris Travers.
246              
247             This program is distributed under the (Revised) BSD License:
248             L
249              
250             Redistribution and use in source and binary forms, with or without
251             modification, are permitted provided that the following conditions
252             are met:
253              
254             * Redistributions of source code must retain the above copyright
255             notice, this list of conditions and the following disclaimer.
256              
257             * Redistributions in binary form must reproduce the above copyright
258             notice, this list of conditions and the following disclaimer in the
259             documentation and/or other materials provided with the distribution.
260              
261             * Neither the name of Chris Travers's Organization
262             nor the names of its contributors may be used to endorse or promote
263             products derived from this software without specific prior written
264             permission.
265              
266             THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
267             "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
268             LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
269             A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
270             OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
271             SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
272             LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
273             DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
274             THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
275             (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
276             OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
277              
278              
279             =cut
280              
281             1; # End of App::PGMultiDeploy