2 # $Id: adddepend,v 1.3 2002/12/02 00:28:29 tgl Exp $
4 # Project exists to assist PostgreSQL users with their structural upgrade
5 # from 7.2 (or prior) to 7.3 (possibly later). Must be run against a 7.3
6 # database system (dump, upgrade daemon, restore, run this script)
8 # - Replace old style Foreign Keys with new style
9 # - Replace old SERIAL columns with new ones
10 # - Replace old style Unique Indexes with new style Unique Constraints
15 # Copyright (c) 2001, Rod Taylor
16 # All rights reserved.
18 # Redistribution and use in source and binary forms, with or without
19 # modification, are permitted provided that the following conditions
22 # 1. Redistributions of source code must retain the above copyright
23 # notice, this list of conditions and the following disclaimer.
25 # 2. Redistributions in binary form must reproduce the above
26 # copyright notice, this list of conditions and the following
27 # disclaimer in the documentation and/or other materials provided
28 # with the distribution.
30 # 3. Neither the name of the InQuent Technologies Inc. nor the names
31 # of its contributors may be used to endorse or promote products
32 # derived from this software without specific prior written
35 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36 # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD
39 # PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52 # Fetch the connection information from the local environment
53 my $dbuser = $ENV{'PGUSER'};
54 $dbuser ||= $ENV{'USER'};
56 my $database = $ENV{'PGDATABASE'};
57 $database ||= $dbuser;
60 my $dbhost = $ENV{'PGHOST'};
63 my $dbport = $ENV{'PGPORT'};
71 # Whats the name of the binary?
73 $basename =~ s|.*/([^/]+)$|$1|;
75 ## Process user supplied arguments.
76 for( my $i=0; $i <= $#ARGV; $i++ ) {
77 ARGPARSE: for ( $ARGV[$i] ) {
78 /^-d$/ && do { $database = $ARGV[++$i];
83 /^-[uU]$/ && do { $dbuser = $ARGV[++$i];
90 /^-h$/ && do { $dbhost = $ARGV[++$i]; last; };
91 /^-p$/ && do { $dbport = $ARGV[++$i]; last; };
93 /^--password=/ && do { $dbpass = $ARGV[$i];
94 $dbpass =~ s/^--password=//g;
98 /^-Y$/ && do { $yes = 1; last; };
100 /^-\?$/ && do { usage(); last; };
101 /^--help$/ && do { usage(); last; };
105 # If no arguments were set, then tell them about usage
109 No arguments set. Use '$basename --help' for help
111 Connecting to database '$database' as user '$dbuser'
117 my $dsn = "dbi:Pg:dbname=$database";
118 $dsn .= ";host=$dbhost" if ( "$dbhost" ne "" );
119 $dsn .= ";port=$dbport" if ( "$dbport" ne "" );
121 # Database Connection
122 # -------------------
123 my $dbh = DBI->connect($dsn, $dbuser, $dbpass);
125 # We want to control commits
126 $dbh->{'AutoCommit'} = 0;
130 SET search_path = public;
132 my $sth = $dbh->prepare($sql);
137 SET autocommit TO 'on';
139 my $sth2 = $dbh->prepare($sql2);
143 $dbh->disconnect() if $dbh;
146 findUniqueConstraints();
150 # Find old style Foreign Keys based on:
152 # - Group of 3 triggers of the appropriate types
160 WHERE NOT EXISTS (SELECT *
162 JOIN pg_constraint as c ON (refobjid = c.oid)
163 WHERE objid = pg_trigger.oid
171 my $sth = $dbh->prepare($sql);
172 $sth->execute() || triggerError($!);
174 while (my $row = $sth->fetchrow_hashref)
177 my $fkeynargs = $row->{'tgnargs'};
178 my $fkeyargs = $row->{'tgargs'};
179 my $matchtype = "MATCH SIMPLE";
183 if ($fkeynargs % 2 == 0 && $fkeynargs >= 6) {
191 ) = split(/\000/, $fkeyargs);
193 # Account for old versions which don't seem to handle NULL
194 # but instead return a string. Newer DBI::Pg drivers
195 # don't have this problem
196 if (!defined($ftable)) {
204 ) = split(/\\000/, $fkeyargs);
208 # Clean up the string for further manipulation. DBD doesn't deal well with
209 # strings with NULLs in them
210 $fkeyargs =~ s|\000|\\000|g;
213 # Catch and record MATCH FULL
214 if ($unspecified eq "FULL")
216 $matchtype = "MATCH FULL";
219 # Start off our column lists
220 my $key_cols = "$lcolumn_name";
221 my $ref_cols = "$fcolumn_name";
223 # Perhaps there is more than a single column
224 while ($lcolumn_name = shift(@junk) and $fcolumn_name = shift(@junk)) {
225 $key_cols .= ", $lcolumn_name";
226 $ref_cols .= ", $fcolumn_name";
234 JOIN pg_proc ON (pg_proc.oid = tgfoid)
235 JOIN pg_class ON (pg_class.oid = tgrelid)
239 my $tgsth = $dbh->prepare($trigsql);
240 $tgsth->execute($fkeyargs) || triggerError($!);
242 while (my $tgrow = $tgsth->fetchrow_hashref)
244 my $trigname = $tgrow->{'tgname'};
245 my $tablename = $tgrow->{'relname'};
246 my $fname = $tgrow->{'proname'};
250 /^RI_FKey_cascade_del$/ && do {$deletetype = "ON DELETE CASCADE"; last;};
251 /^RI_FKey_cascade_upd$/ && do {$updatetype = "ON UPDATE CASCADE"; last;};
252 /^RI_FKey_restrict_del$/ && do {$deletetype = "ON DELETE RESTRICT"; last;};
253 /^RI_FKey_restrict_upd$/ && do {$updatetype = "ON UPDATE RESTRICT"; last;};
254 /^RI_FKey_setnull_del$/ && do {$deletetype = "ON DELETE SET NULL"; last;};
255 /^RI_FKey_setnull_upd$/ && do {$updatetype = "ON UPDATE SET NULL"; last;};
256 /^RI_FKey_setdefault_del$/ && do {$deletetype = "ON DELETE SET DEFAULT"; last;};
257 /^RI_FKey_setdefault_upd$/ && do {$updatetype = "ON UPDATE SET DEFAULT"; last;};
258 /^RI_FKey_noaction_del$/ && do {$deletetype = "ON DELETE NO ACTION"; last;};
259 /^RI_FKey_noaction_upd$/ && do {$updatetype = "ON UPDATE NO ACTION"; last;};
262 $triglist .= " DROP TRIGGER \"$trigname\" ON $tablename;\n";
267 if ($keyname ne "<unnamed>")
269 $constraint = "CONSTRAINT \"$keyname\"";
274 ALTER TABLE $table ADD $constraint FOREIGN KEY ($key_cols)
275 REFERENCES $ftable($ref_cols) $matchtype $updatetype $deletetype;
278 # Does the user want to upgrade this sequence?
280 The below commands will upgrade the foreign key style. Shall I execute them?
286 my $sthfkey = $dbh->prepare($fkey);
287 $sthfkey->execute() || $dbh->rollback();
288 $dbh->commit() || $dbh->rollback();
295 # Find possible old style Serial columns based on:
297 # - Process unique constraints. Unique indexes without
298 # the corresponding entry in pg_constraint)
299 sub findUniqueConstraints
302 SELECT ci.relname AS index_name
303 , ct.relname AS table_name
304 , pg_catalog.pg_get_indexdef(indexrelid) AS constraint_definition
306 JOIN pg_index ON (ci.oid = indexrelid)
307 JOIN pg_class AS ct ON (ct.oid = indrelid)
308 JOIN pg_catalog.pg_namespace ON (ct.relnamespace = pg_namespace.oid)
310 AND NOT EXISTS (SELECT TRUE
311 FROM pg_catalog.pg_depend
312 JOIN pg_catalog.pg_constraint ON (refobjid = pg_constraint.oid)
313 WHERE objid = indexrelid
315 AND nspname NOT IN ('pg_catalog', 'pg_toast');
318 my $sth = $dbh->prepare($sql) || triggerError($!);
321 while (my $row = $sth->fetchrow_hashref)
324 my $constraint_name = $row->{'index_name'};
325 my $table = $row->{'table_name'};
326 my $columns = $row->{'constraint_definition'};
328 # Extract the columns from the index definition
329 $columns =~ s|.*\(([^\)]+)\).*|$1|g;
330 $columns =~ s|([^\s]+)[^\s]+_ops|$1|g;
333 DROP INDEX $constraint_name RESTRICT;
334 ALTER TABLE $table ADD CONSTRAINT $constraint_name UNIQUE ($columns);
338 # Does the user want to upgrade this sequence?
342 Upgrade the Unique Constraint style via:
348 # Drop the old index and create a new constraint by the same name
350 my $upsth = $dbh->prepare($upsql);
351 $upsth->execute() || $dbh->rollback();
353 $dbh->commit() || $dbh->rollback();
359 # Find possible old style Serial columns based on:
361 # - Column is int or bigint
362 # - Column has a nextval() default
363 # - The sequence name includes the tablename, column name, and ends in _seq
364 # or includes the tablename and is 40 or more characters in length.
372 FROM pg_catalog.pg_class as c
374 JOIN pg_catalog.pg_attribute as a
375 ON (c.oid = a.attrelid)
377 JOIN pg_catalog.pg_attrdef as ad
378 ON (a.attrelid = ad.adrelid
379 AND a.attnum = ad.adnum)
381 JOIN pg_catalog.pg_type as t
382 ON (t.typname IN ('int4', 'int8')
383 AND t.oid = a.atttypid)
385 JOIN pg_catalog.pg_namespace as n
386 ON (c.relnamespace = n.oid)
388 WHERE n.nspname = 'public'
389 AND adsrc LIKE 'nextval%'
390 AND adsrc LIKE '%'|| relname ||'_'|| attname ||'_seq%'
391 AND NOT EXISTS (SELECT *
392 FROM pg_catalog.pg_depend as sd
393 JOIN pg_catalog.pg_class as sc
394 ON (sc.oid = sd.objid)
395 WHERE sd.refobjid = a.attrelid
396 AND sd.refobjsubid = a.attnum
400 AND sc.relname = c.relname ||'_'|| a.attname || '_seq'
404 my $sth = $dbh->prepare($sql) || triggerError($!);
407 while (my $row = $sth->fetchrow_hashref)
410 my $table = $row->{'relname'};
411 my $column = $row->{'attname'};
412 my $seq = $row->{'adsrc'};
414 # Extract the sequence name from the default
415 $seq =~ s|^nextval\(["']+([^'"\)]+)["']+.*\)$|$1|g;
417 # Does the user want to upgrade this sequence?
419 Do you wish to upgrade Sequence '$seq' to SERIAL?
420 Found on column $table.$column
425 # Add the pg_depend entry for the serial column. Should be enough
426 # to fool pg_dump into recreating it properly next time. The default
427 # is still slightly different than a fresh serial, but close enough.
429 INSERT INTO pg_catalog.pg_depend
437 ) VALUES ( (SELECT c.oid -- classid
439 JOIN pg_namespace as n
440 ON (n.oid = c.relnamespace)
441 WHERE n.nspname = 'pg_catalog'
442 AND c.relname = 'pg_class')
444 , (SELECT c.oid -- objid
446 JOIN pg_namespace as n
447 ON (n.oid = c.relnamespace)
448 WHERE n.nspname = 'public'
449 AND c.relname = '$seq')
453 , (SELECT c.oid -- refclassid
455 JOIN pg_namespace as n
456 ON (n.oid = c.relnamespace)
457 WHERE n.nspname = 'pg_catalog'
458 AND c.relname = 'pg_class')
460 , (SELECT c.oid -- refobjid
462 JOIN pg_namespace as n
463 ON (n.oid = c.relnamespace)
464 WHERE n.nspname = 'public'
465 AND c.relname = '$table')
467 , (SELECT a.attnum -- refobjsubid
469 JOIN pg_namespace as n
470 ON (n.oid = c.relnamespace)
471 JOIN pg_attribute as a
472 ON (a.attrelid = c.oid)
473 WHERE n.nspname = 'public'
474 AND c.relname = '$table'
475 AND a.attname = '$column')
481 my $upsth = $dbh->prepare($upsql);
482 $upsth->execute() || $dbh->rollback();
484 $dbh->commit() || $dbh->rollback();
492 # Wait for a key press
498 # Sleep for key unless -Y was used
505 # Wait for a keypress
508 print "\n << 'Y'es or 'N'o >> : ";
513 # If it's not a Y or N, then ask again
514 $key =~ s/[^YyNn]//g;
527 # Exit nicely, but print a message as we go about an error
532 # Set a default message if one wasn't supplied
535 $msg = "Unknown error";
551 $basename [options] [dbname [username]]
554 -d <dbname> Specify database name to connect to (default: $database)
555 -h <host> Specify database server host (default: localhost)
556 -p <port> Specify database server port (default: 5432)
557 -u <username> Specify database username (default: $dbuser)
558 --password=<pw> Specify database password (default: blank)
560 -Y The script normally asks whether the user wishes to apply
561 the conversion for each item found. This forces YES to all