3 # Update the APMS database by repeatedly applying patches to it
4 # in the correct order.
11 use Getopt
::Long
qw(:config permute); # allow mixed args.
20 my $appuser = "apms_app";
22 my $apply_patches = 1;
25 my $reset_sequences = 0;
26 my $config_file = "config/administration.yml";
29 $dbadir =~ s
#/[^/]*$##;
30 my $patchdir = $dbadir . "/patches";
33 # We look in a few places for the config file. First relative to
34 # where we are, then relative to the code we are running, then we
35 # start to look in absolute locations. Then we give up :-)
36 if ( ! -f
$config_file ) {
38 $config_file =~ s{[^/]+/update-[a-z]+-database}{config/administration.yml};
40 if ( ! -f
$config_file ) {
41 $config_file = "/etc/apms/administration.yml";
43 if ( -f
$config_file ) {
44 use YAML
qw( LoadFile );
46 my ($ycfg) = LoadFile
($config_file);
47 $dbuser = $ycfg->{'admin_db_user'} if ( defined($ycfg->{'admin_db_user'}));
48 $dbpass = $ycfg->{'admin_db_pass'} if ( defined($ycfg->{'admin_db_pass'}));
49 $dbhost = $ycfg->{'admin_db_host'} if ( defined($ycfg->{'admin_db_host'}));
50 $dbname = $ycfg->{'admin_db_name'} if ( defined($ycfg->{'admin_db_name'}));
51 $dbport = $ycfg->{'admin_db_port'} if ( defined($ycfg->{'admin_db_port'}));
52 $appuser = $ycfg->{'app_db_user'} if ( defined($ycfg->{'app_db_user'}));
55 GetOptions
('debug!' => \
$debug,
56 'dbname=s' => \
$dbname,
57 'dbuser=s' => \
$dbuser,
58 'dbpass=s' => \
$dbpass,
59 'dbport=s' => \
$dbport,
60 'dbhost=s' => \
$dbhost,
61 'appuser=s' => \
$appuser,
62 'patch!' => \
$apply_patches,
63 'owner=s' => \
$force_owner,
64 'resequence!' => \
$reset_sequences,
65 'revoke=s' => \
$revoke_list,
66 'help' => \
$helpmeplease );
68 show_usage
() if ( $helpmeplease );
70 $revoke_list = ", ". $revoke_list if ( $revoke_list ne "" );
73 ############################################################
74 # Open database connection. Note that the standard PostgreSQL
75 # environment variables will also work with DBD::Pg.
76 ############################################################
77 my $dsn = "dbi:Pg:dbname=$dbname";
78 $dsn .= ";host=$dbhost" if ( "$dbhost" ne "" );
79 $dsn .= ";port=$dbport" if ( $dbport != 5432 );
81 print "Using database: $dbuser".'%'.$dbpass.'@'.$dsn."\n" if ( $debug );
84 my $last_results = ''; # Will hold the last SQL result from applying a patch
86 if ( $apply_patches ) {
87 $current_revision = get_current_revision
();
88 printf( "The database is currently at revision %d.%d.%d.\n", $current_revision->{'schema_major'}, $current_revision->{'schema_minor'}, $current_revision->{'schema_patch'} );
90 opendir( PATCHDIR
, $patchdir ) or die "Can't open patch directory $patchdir";
91 my @patches = grep { /^([0-9]+)\.([0-9]+)\.([0-9]+)([a-z]?)\.sql$/ } readdir(PATCHDIR
);
94 @patches = sort { compare_revisions
(revision_hash
($a),revision_hash
($b), 1); } @patches;
98 for ( my $i=0; $i <= $#patches; $i++ ) {
99 printf( "Looking at patches[%d] (%s)\n", $i, $patches[$i]) if ( $debug );
100 if ( compare_revisions
(revision_hash
($patches[$i]),$current_revision) > 0 ) {
101 print "Applying patch $patches[$i] ... ";
102 if ( !apply_patch
( $patches[$i] ) ) {
103 # Skip to the end unless the next patch is an alternate for the same version.
104 if ( defined($patches[$i+1]) && compare_revisions
(revision_hash
($patches[$i]),revision_hash
($patches[$i+1])) == 0 ) {
105 print "failed. Attempting next alternative.\n";
109 print "failed!\n$last_results ==> No further patches will be attempted!\n";
114 print "succeeded.\n";
119 print "Patch $patches[$i] has already been applied.\n" if ( $debug );
124 print "Successfully applied $applied patches.\n";
127 print "No patches were applied.\n";
131 # Ensure the locales data is up to date
132 apply_sql_file
( $dbadir, "supported_locales.sql" );
133 print "Supported locales updated.\n";
135 # Ensure the functions are up to date
136 apply_sql_file
( $dbadir, "functions.sql" );
137 print "APMS functions updated.\n";
139 # Ensure the permissions are up to date
140 apply_permissions
( $dbadir, "appuser_permissions.txt" );
141 print "Database permissions updated.\n";
143 # Ensure the sequence values are reasonable
144 set_table_sequences
( $dbadir, "apms_sequences.txt" );
145 print "Database sequences updated.\n";
153 ############################################################
154 # Revision Hash - we either have a single parameter,
155 # which is of the form "1.2.3" or we have three parameters.
156 ############################################################
158 my $rev = +{ 'schema_major', => 0, 'schema_minor' => 0, 'schema_patch' => 0, 'alternative' => '0' };
160 return $rev unless ( defined($first) );
161 if ( $first =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)([a-z]?)([^0-9]|$)/ ) {
162 $rev->{'schema_major'} = $1;
163 $rev->{'schema_minor'} = $2;
164 $rev->{'schema_patch'} = $3;
165 $rev->{'alternative'} = $4;
168 $rev->{'schema_major'} = $first;
169 $rev->{'schema_minor'} = shift;
170 $rev->{'schema_patch'} = shift;
171 $rev->{'alternative'} = '0';
177 ############################################################
179 ############################################################
180 sub compare_revisions
{
183 my $test_alt = shift;
185 return -1 if ( $a->{'schema_major'} < $b->{'schema_major'} );
186 return 1 if ( $a->{'schema_major'} > $b->{'schema_major'} );
188 return -1 if ( $a->{'schema_minor'} < $b->{'schema_minor'} );
189 return 1 if ( $a->{'schema_minor'} > $b->{'schema_minor'} );
191 return -1 if ( $a->{'schema_patch'} < $b->{'schema_patch'} );
192 return 1 if ( $a->{'schema_patch'} > $b->{'schema_patch'} );
194 if ( defined($test_alt) ) {
195 return -1 if ( $a->{'alternative'} lt $b->{'alternative'} );
196 return 1 if ( $a->{'alternative'} gt $b->{'alternative'} );
205 ############################################################
206 # Get the current revision
207 ############################################################
208 sub get_current_revision
{
210 my $dbh = DBI
->connect($dsn, $dbuser, $dbpass, { AutoCommit
=> 0 } ) or die "Can't connect to database $dbname";
212 my $current_revision = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
213 SELECT schema_major, schema_minor, schema_patch FROM awl_db_revision ORDER BY schema_id DESC LIMIT 1
216 if ( $current_revision->execute() ) {
217 my $revision = $current_revision->fetchrow_hashref();
218 undef $current_revision;
223 die "ERROR: Cannot read current revision from database.";
230 ############################################################
231 # Apply a DB Patch File
232 ############################################################
237 apply_sql_file
( $patchdir, $patch );
239 $current_revision = get_current_revision
();
240 if ( compare_revisions
($current_revision,revision_hash
($patch)) != 0 ) {
241 printf( "Failed to apply revision %s to the database!\n", $patch ) if ( $debug );
249 ############################################################
251 # Note that this stuffs the password into an environment
252 # variable, which isn't ideal. If you use a .pgpass you
253 # can bypass that issue, but you still need it on the command
254 # line for this program until I get a patch from someone.
255 ############################################################
261 my @psql_opts = ( "psql", "-q", "-f", $sqldir."/".$sqlfile, $dbname );
262 push @psql_opts, "-h", $dbhost if ( $dbhost ne "" );
263 push @psql_opts, "-p", "$dbport" if ( $dbport != 5432 );
264 push @psql_opts, "-U", $dbuser if ( $dbuser ne "" );
265 $ENV{'PGPASS'} = $dbpass if ( $dbpass ne "" );
267 my $command = join ' ', @psql_opts;
268 $last_results = `$command 2>&1 1>/dev/null`;
270 $last_results =~ s/^.*WARNING: there is no transaction in progress\s$//m;
271 $last_results =~ s/^.*NOTICE: //m;
276 ############################################################
277 # Apply database permissions from file
278 ############################################################
279 sub apply_permissions
{
282 my $permsfile = shift;
284 open PERMS
, '<', $sqldir."/".$permsfile;
285 my $dbh = DBI
->connect($dsn, $dbuser, $dbpass, { AutoCommit
=> 1 } ) or die "Can't connect to database $dbname";
291 next if ( /^\s*(#|--)/ );
293 /^\s*GRANT\s+(\S.*)\s*$/i && do {
297 /^\s*ON\s+(\S.*)\s*$/i && do {
298 defined($current_grant) or die "No GRANT before ON in $permsfile\n";
301 if ( $revoke_list ne "" ) {
302 # TODO: we should really loop through the revoke_list so that a single non-existent
303 # user doesn't cause this whole statement to fail.
304 $sql = sprintf( "REVOKE ALL ON %s FROM %s %s", $doohickey, $appuser, $revoke_list );
305 print $sql, "\n" if ( $debug );
309 $sql = sprintf( "GRANT %s on %s to %s", $current_grant, $doohickey, $appuser );
310 print $sql, "\n" if ( $debug );
313 if ( $force_owner ne "" ) {
314 if ( $doohickey =~ /_seq$/ ) {
315 $sql = sprintf( "GRANT ALL on %s to %s", $doohickey, $force_owner );
318 $sql = sprintf( "ALTER TABLE %s OWNER to %s", $doohickey, $force_owner );
320 print $sql, "\n" if ( $debug );
332 ################################################################
333 # Set sequences for tables based on a file of table,column pairs
334 ################################################################
335 sub set_table_sequences
{
337 my $seqsfile = shift;
339 open SEQS
, '<', $sqldir."/".$seqsfile;
340 my $dbh = DBI
->connect($dsn, $dbuser, $dbpass, { AutoCommit
=> 1 } ) or die "Can't connect to database $dbname";
342 my $current_sequence = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
343 SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
344 FROM pg_catalog.pg_attrdef d
345 JOIN pg_catalog.pg_attribute a ON (d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)
346 JOIN pg_catalog.pg_class c ON (c.oid = a.attrelid)
347 WHERE c.relname = ? AND a.attname = ?
350 my $maximum_vaue = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
358 next if ( /^\s*(#|--)/ );
360 if ( m{^\s*(\S+)\s*,\s*(\S+)\s*$} ) {
364 $current_sequence->execute( $tablename, $columname ) or die "ERROR: Cannot read current sequence for column $tablename.$columname from database.";
365 my $seqdef = $current_sequence->fetchall_arrayref();
366 $seqdef = $seqdef->[0][0];
368 $sql = "SELECT $columname FROM $tablename ORDER BY 1 DESC LIMIT 1";
369 print STDERR
$sql, "\n" if ( $debug );
370 my $maxval_sth = $dbh->selectcol_arrayref( $sql ) or die "ERROR: Cannot read current maximum for column $tablename.$columname from database.";
372 $maxval = $maxval_sth->[0] if ( defined($maxval_sth) );
374 if ( $tablename eq 'newbatch' ) {
375 $sql = "SELECT $columname FROM batch ORDER BY 1 DESC LIMIT 1";
376 print STDERR
$sql, "\n" if ( $debug );
377 my $maxval_sth = $dbh->selectcol_arrayref( $sql ) or die "ERROR: Cannot read current maximum for column batch.$columname from database.";
378 my $maxbatch = $maxval_sth->[0] if ( defined($maxval_sth) );
379 $maxval = $maxbatch if ( !defined($maxval) || $maxbatch > $maxval );
382 if ( defined($seqdef) && $seqdef =~ m{^nextval\('(.+)'} ) {
384 $sql = "select last_value FROM $seqname";
385 print STDERR
$sql, "\n" if ( $debug );
386 my $seq_last_sth = $dbh->selectcol_arrayref( $sql ) or die "ERROR: Cannot read last value of sequence $seqname for ${tablename}.${columname}";
388 $seq_last = $seq_last_sth->[0] if ( defined($seq_last_sth) );
389 if ( $reset_sequences || !defined($maxval) || !defined($seq_last) || $seq_last < $maxval ) {
390 if ( defined($maxval) && $maxval > 0 ) {
391 $sql = "select setval(?,$maxval)";
394 $sql = "select setval(?,1,false)";
396 print STDERR
$sql, "\n" if ( $debug );
397 $dbh->do( $sql, {}, ($seqname) ) or die "ERROR: Cannot set appropriate value for sequence $seqname for ${tablename}.${columname}";
402 $start = "START $maxval" if ( defined($maxval) && $maxval >= 1 );
403 $sql = "CREATE SEQUENCE ${tablename}_${columname}_seq $start OWNED BY ${tablename}.${columname}";
404 print STDERR
$sql, "\n" if ( $debug );
405 $dbh->do( $sql ) or die "ERROR: Cannot create sequence ${tablename}_${columname}_seq.";
406 $sql = "ALTER TABLE ${tablename} ALTER COLUMN ${columname} SET DEFAULT nextval('${tablename}_${columname}_seq')";
407 print STDERR
$sql, "\n" if ( $debug );
408 $dbh->do( $sql ) or die "ERROR: Cannot assign sequence ${tablename}_${columname}_seq.as default for $tablename.$columname";
419 ############################################################
420 # Tell the nice user how we do things. Short and sweet.
421 ############################################################
425 update-rscds-database [options]
428 --debug Turn on debugging
429 --dbname name The database to dig into
430 --dbuser name Connect to the database as this user.
431 --dbport 5432 Connect to the database on this port.
432 --dbhost name Connect to the database on this host.
433 --appuser name The database username which the application uses for it's
435 --owner name The database username which is used for administrative
436 access to the database. This option forces the tables
437 to be owned by this user (default: not present).
438 --nopatch Don't try and apply any patches
439 --revoke name Revoke permissions from this user
441 The program will apply any patches to the database which have
442 not yet been applied, run any desired data patch scripts and set
443 the correct minimum permissions for the web application user.
445 Rather than providing a password on the command-line it is recommended
446 that you use a .pgpass file in your home directory to hold the database
447 password. This file must be mode 600 to work.