root/trunk/getddl/getddl.pl

Revision 24, 23.5 kB (checked in by robert, 5 years ago)

add option for alternative svn paths, dump functions, tables to thier own directories

Line 
1 #!/data/bin/perl
2 use strict;
3 use warnings;
4
5 # getddl, a script for managing postgresql schema via svn
6 # Copyright 2008, OmniTI, Inc. (http://www.omniti.com/)
7 # See complete license and copyright information at the bottom of this script 
8 # For newer versions of this script, please see:
9 # https://labs.omniti.com/trac/pgsoltools/wiki/getddl
10 # POD Documentation also available by issuing pod2text getddl.pl
11
12 use DBI;
13 use Data::Dumper;
14 use Getopt::Long;
15 use DirHandle;
16
17 package GetDDL::SuppList;
18 sub new {
19     my ($class, %args) = @_;
20     my $self = bless {}, $class;
21     $self->{data} = '';
22     $self->{suppress} = sub { 0 };
23     $self->load($args{filename}) if $args{filename};
24     return $self;
25 }
26 sub load {
27     my ($self, $fn) = @_;
28     my $fh = do { no warnings; local *FH };
29     open $fh, "< $fn" or die "couldn't read [$fn]: $!\n";
30     { local $/ = undef; $self->{data} = <$fh> }
31     close $fh;
32     my $evalme = 'sub {';
33     for my $line (split /[\r\n]+/, $self->{data}) {
34         if ($line =~ /^=~/) {
35             # reject if matches expression
36             $line =~ s/(?<!;)$/;/;
37             $evalme .= "return 1 if \$_[0] $line";
38         } else {
39             # reject if contains substring
40             $evalme .= "return 1 if index(\$_[0], '$line') >= 0;";
41         }
42     }
43     $evalme .= '0}';
44     $self->{codestr} = $evalme;
45     $self->{suppress} = eval $evalme;
46     return;
47 }
48 package main;
49
50 # no svn interaction by default, just write 'em out
51 my ($DO_SVN, $WRITE_DDL, $QUIET, $DDL_BASE) = (0, 1, 0, './');
52 my ($GET_DDL, $GET_PROCS) = (0, 0);
53 my ($tsfn, $fsfn) = (undef, undef);
54 my ($tso, $fso) = (undef, undef);
55 our (@hosts, @schemas) = ();
56 my $commit_msg = 'Pg ddl updates';
57 my $fn = '/var/tmp/'.(time).".$$";
58 my $default_fn = $fn;
59 my $svnuser = "--username postgres --password password";
60 my (@tables_found, @procs_found);
61 my $do_svn_del = 0;
62 my $svn = '/opt/omni/bin/svn';
63
64 die unless GetOptions(
65     'svn!' => \$DO_SVN,
66     'writeddl!' => \$WRITE_DDL,
67     'ddlbase=s' => \$DDL_BASE,
68     'host=s' => \@hosts,
69     'schema=s' => \@schemas,
70     'commitmsg=s' => \$commit_msg,
71     'quiet!' => \$QUIET,
72     'getprocs!' => \$GET_PROCS,
73     'getddl!' => \$GET_DDL,
74     'commitmsgfn' => \$fn,
75     'tsuppfn=s' => \$tsfn,
76     'fsuppfn=s' => \$fsfn,
77     'svndel' => \$do_svn_del,
78     'svndir=s' => \$svn,
79 );
80 exit if not $GET_DDL and not $GET_PROCS; # nothing to do
81 $DO_SVN = 0 if $WRITE_DDL == 0; # can't compare if we don't write to disk
82 $default_fn = 0 if $fn ne $default_fn;
83
84 $tso = GetDDL::SuppList->new();
85 $tso->load($tsfn) if $tsfn;
86 $fso = GetDDL::SuppList->new();
87 $fso->load($fsfn) if $fsfn;
88
89 my ($DSN, $username, $password, $destdir);
90 my $svn = '/opt/omni/bin/svn';
91 my $real_server_name=`hostname`;
92 my $curhost = chomp($real_server_name);
93 my $iters = scalar @hosts;
94 $curhost = shift @hosts if $iters;
95 if (not $iters) {
96     $iters=1;
97     print STDERR "host will default to core-0-3, continue? [y/n] (n): ";
98     exit if <STDIN> !~ /y/i;
99 }
100
101 my $start_time = time();
102
103 sub elapsed_time { return time() - $start_time; }
104
105    
106 my (@to_commit, @to_add);
107 my ($dbh,
108     $tables_h, $columns_h, $constraints_h, $indexes_h, $triggers_h,
109     $functions_h, $getnumargs_h, $funcargs_h);
110
111 # patterns to match names we don't care about
112 my @reject = ();
113 # (lowercase) strings to match the exact names we care about
114 my @only = ();
115
116 ## Fixme - this needs to account for the new data type specific directories. also i dont think it works against remote hosts, but could.
117 my $schema_check = sub {
118     my ($fqn) = @_;
119     if (not scalar @schemas) { return 1 }
120     my ($schema, $objname) = split /\./, $fqn;
121     my @hschemas = grep(/^$curhost\.$schema/ || !/\./, @schemas);
122     for my $s (@hschemas) {
123         return 1
124             if $schema eq $s or $schema eq substr($s, index($s, '.')+1);
125     }
126     return 0;
127 };
128
129 sub svn_check {
130     my (%args) = @_;
131     my $fn = "$args{destdir}/$args{fqn}.sql";
132     my $fh = do { no warnings; local *FH };
133     open $fh, "> $fn" or (warn("couldn't create [$fn]: $!\n") && next);
134     print $fh $args{ddl};
135     close $fh;
136     chmod 0664, $fn;
137
138     print "  * comparing $args{fqn}\n" if not $QUIET;
139     # svn st, ? = add, m = commit
140     my $svnst = `$args{svn} st $svnuser $args{destdir}`;
141     for my $line (split "\n", $svnst) {
142         next if $line !~ /\.sql$/;
143         if ($line =~ /^\?\s+(\S+)$/) {
144             $fn = $1;
145             if (not $DO_SVN) {
146                 print("svn add $fn\n") if not $QUIET;
147             } else {
148                 push @{$args{to_add}}, $fn;
149             }
150         } elsif ($line =~ /^M\s+\Q$fn/) {
151            if (not $DO_SVN) {
152               print("svn commit $svnuser $fn\n") if not $QUIET;
153            } else {
154               push @{$args{to_commit}}, $fn;
155            }
156         }
157     }
158 }
159
160 # define the sql to pull all the information
161 # we need in order to recreate the ddl
162 my $tables = "
163     SELECT table_schema, table_name
164       FROM information_schema.tables
165      WHERE table_type = 'BASE TABLE'
166        AND table_schema NOT IN ('pg_catalog', 'information_schema')
167      ORDER BY table_schema, table_name
168 ";
169     my $columns = q"
170         SELECT column_name, data_type, column_default, is_nullable,
171                character_maximum_length, numeric_precision, datetime_precision
172           FROM information_schema.columns
173          WHERE table_schema = ? AND table_name = ?
174          ORDER BY ordinal_position
175     ";
176     my $constraints = q"
177         SELECT table_schema, table_name, column_name, constraint_name
178           FROM information_schema.constraint_column_usage
179          WHERE constraint_schema = ? AND constraint_name LIKE ? || '_%'
180          ORDER BY constraint_name
181     ";
182     my $indexes = q"
183         SELECT indexdef
184           FROM pg_indexes
185          WHERE schemaname = ? AND tablename = ? AND indexname NOT LIKE '%_key'
186          ORDER BY indexdef
187     ";
188     my $triggers = q"
189         SELECT trigger_schema, trigger_name, event_manipulation,
190                event_object_schema, event_object_table, action_order,
191                action_condition, action_statement, action_orientation,
192                condition_timing
193           FROM information_schema.triggers
194          WHERE event_object_schema = ? and event_object_table = ?
195          ORDER BY trigger_name
196     ";
197 # and to recreate the function definitions
198 my $functions = q"
199     SELECT external_language, data_type, routine_type,
200            type_udt_name, type_udt_schema,
201            routine_schema, routine_name, routine_definition
202       FROM information_schema.routines r
203       WHERE routine_schema NOT IN ('pg_catalog', 'information_schema')
204      ORDER BY routine_schema, routine_name
205 ";
206     my $getnumargs = q"
207         SELECT
208                CASE WHEN p.proallargtypes IS NULL
209                THEN array_lower(p.proargtypes,1)+1
210                ELSE array_lower(p.proallargtypes,1) END as idx_min,
211                CASE WHEN p.proallargtypes IS NULL
212                THEN array_upper(p.proargtypes,1)+1
213                ELSE array_upper(p.proallargtypes,1) END as idx_max,
214                p.pronargs as n, p.proretset as retset
215           FROM pg_catalog.pg_proc p
216           JOIN pg_catalog.pg_namespace n
217             ON p.pronamespace = n.oid
218          WHERE p.proname = ? AND n.nspname = ?
219     ";
220     my $funcargs = q"
221         SELECT coalesce(pg_catalog.format_type(p.proallargtypes[i.idx], NULL),
222                         pg_catalog.format_type(p.proargtypes[i.idx-1], NULL)) as typename,
223                CASE
224                WHEN p.proallargtypes IS NULL THEN 'i'
225                ELSE p.proargmodes[i.idx] END as iomode,
226                p.proargnames[i.idx] as argname
227           FROM pg_catalog.pg_proc p
228           JOIN pg_catalog.pg_namespace n
229             ON p.pronamespace = n.oid,
230                (select $1::integer as idx) i
231          WHERE p.proname = $2::varchar AND n.nspname = $3::varchar
232     ";
233 # TODO: get user-defined types
234 for (1 .. $iters) {
235     if ($curhost =~ 'core-0-') {
236         $DSN = 'dbname=pagila;host=localhost;';
237         $username = 'postgres';
238         $password = 'password';
239         $destdir = "$DDL_BASE/$curhost";
240     } else {
241         # not understood
242         warn "server nickname '$curhost' not understood, skipping\n";
243         next;
244     }
245
246     # make sure directory exists
247     if ($destdir) {
248         if (!-e $destdir) {
249             mkdir $destdir or die "couldn't create directory target [$destdir]: $!\n";
250             print "created directory target [$destdir]\n";
251         } elsif (!-d $destdir) {
252             die "directory target [$destdir] conflicts with existing file!\n"
253         }
254     }
255
256     $dbh = DBI->connect("dbi:Pg:$DSN", $username, $password) or die "$DBI::errstr\n";
257     print "in $destdir\n" if not $QUIET;
258     goto GET_PROCS if not $GET_DDL;
259
260     # this is what we loop over
261     $tables_h = $dbh->prepare($tables) or die "died preparing: $DBI::errstr\n";
262     $tables_h->execute() or die "died executing: $DBI::errstr\n";
263
264     # these get executed for each table
265     $columns_h = $dbh->prepare($columns) or die "died preparing: $DBI::errstr\n";
266     $constraints_h = $dbh->prepare($constraints) or die "died preparing: $DBI::errstr\n";
267     $indexes_h = $dbh->prepare($indexes) or die "died preparing: $DBI::errstr\n";
268     $triggers_h = $dbh->prepare($triggers) or die "died preparing: $DBI::errstr\n";
269
270   TABLE_ROW:
271     while (my $table_row = $tables_h->fetchrow_hashref()) {
272         my $fqtn = "$table_row->{table_schema}.$table_row->{table_name}";
273
274         # Add the table to the list of tables found, even if we don't end up
275         # processing it.
276         push(@tables_found, $fqtn);
277
278         # hardcoded rejection
279         ($table_row->{table_name} =~ /$_/) && next TABLE_ROW for @reject;
280         # only get specific tables
281         # TODO: make inclusive filtering a cmd-line option
282         next TABLE_ROW if scalar(@only) and not (grep(lc $table_row->{table_name}, @only) and
283              grep(lc $fqtn, @only));
284         # only get host.schema specified on cmd-line
285         next TABLE_ROW if not $schema_check->($fqtn);
286         next if $tso->{suppress}->($fqtn);
287
288         # get the columns for this table
289         $columns_h->execute($table_row->{table_schema}, $table_row->{table_name})
290             or die "died executing columns: $DBI::errstr\n";
291         $table_row->{columns} = [];
292         while (my $column_row = $columns_h->fetchrow_hashref()) {
293             push @{$table_row->{columns}}, $column_row;
294         }
295         $columns_h->finish();
296
297         # get the constraints for this table
298         $constraints_h->execute($table_row->{table_schema}, $table_row->{table_name})
299             or die "died executing constraints: $DBI::errstr\n";
300         $table_row->{constraints} = [];
301         while (my $constraint_row = $constraints_h->fetchrow_hashref()) {
302             push @{$table_row->{constraints}}, $constraint_row;
303         }
304         $constraints_h->finish();
305
306         # get the indexes for this table
307         $indexes_h->execute($table_row->{table_schema}, $table_row->{table_name})
308             or die "died executing indexes: $DBI::errstr\n";
309         $table_row->{indexes} = [];
310         while (my $index_row = $indexes_h->fetchrow_hashref()) {
311             push @{$table_row->{indexes}}, $index_row;
312         }
313         $indexes_h->finish();
314
315         # get the triggers for this table
316         $triggers_h->execute($table_row->{table_schema}, $table_row->{table_name})
317             or die "died executing triggers: $DBI::errstr\n";
318         $table_row->{triggers} = [];
319         while (my $trigger_row = $triggers_h->fetchrow_hashref()) {
320             push @{$table_row->{triggers}}, $trigger_row;
321         }
322         $triggers_h->finish();
323
324
325         # build DDL from queries above
326         my $ddl = "CREATE TABLE $fqtn (\n";
327         for my $col (@{$table_row->{columns}}) {
328             my $vclen = $col->{data_type} =~ /^character/
329                 ? "($col->{character_maximum_length})"
330                 : '';
331             $col->{is_nullable} ||= 'NO'; # get rid of annoying uninit warnings
332             my $nil = $col->{is_nullable} eq 'YES' ? ' NULL' : ' NOT NULL';
333             if (defined $col->{column_default} and $col->{column_default} =~ /^nextval\(/) {
334                 if ($col->{data_type} eq 'integer') {
335                     $col->{data_type} = 'serial';
336                     $col->{column_default} = undef;
337                 } elsif ($col->{data_type} eq 'bigint') {
338                     $col->{data_type} = 'bigserial';
339                     $col->{column_default} = undef;
340                 } elsif ($col->{data_type} eq 'smallint') {
341                     # need to manually create the sequence this references
342                     $ddl = "CREATE SEQUENCE $fqtn\_$col->{column_name}_seq;\n".$ddl;
343                 }
344             }
345             my $default = $col->{column_default} ? " DEFAULT $col->{column_default}" : '';
346             $ddl .= "\t$col->{column_name} $col->{data_type}$vclen$nil$default,\n";
347         }
348         my @pkeys = grep $_->{constraint_name} =~ /_pkey$/, @{$table_row->{constraints}};
349         $ddl .= "\tPRIMARY KEY(".join(',', map($_->{column_name}, @pkeys)).")\n" if @pkeys;
350         $ddl =~ s/,(\s+)$/$1/; # get rid of possible last trailing comma
351         $ddl .= ");\n\n";
352         $ddl .= "$_->{indexdef};\n\n" for @{$table_row->{indexes}||[]};
353         my @fkeys = grep($_->{constraint_name} =~ /_fkey$/, @{$table_row->{constraints}});
354         my %fkeys;
355         for my $fkey (@fkeys) {
356             $fkeys{$fkey->{constraint_name}} ||= [];
357             push @{$fkeys{$fkey->{constraint_name}}}, $fkey;
358         }
359         for my $fkey_name (sort keys %fkeys) {
360             my $cols = join(',', map($_->{column_name}, @{$fkeys{$fkey_name}}));
361             my $row = $fkeys{$fkey_name}[0];
362             $ddl .= "ALTER TABLE $fqtn\n".
363                 "\tADD FOREIGN KEY ($cols) REFERENCES $row->{table_schema}.$row->{table_name}".
364                 "($cols);\n\n"
365         }
366         my %triggers;
367         for my $trigger (@{$table_row->{triggers}}) {
368             $triggers{$trigger->{trigger_name}} ||= [];
369             push @{$triggers{$trigger->{trigger_name}}}, $trigger;
370         }
371         for my $trigger_name (sort keys %triggers) {
372             my $events = join(' OR ', map($_->{event_manipulation}, @{$triggers{$trigger_name}}));
373             my $row = $triggers{$trigger_name}[0];
374             $ddl .= "CREATE TRIGGER $row->{trigger_name}\n".
375                 "$row->{condition_timing} $events ON $row->{event_object_schema}.$row->{event_object_table}\n".
376                 "FOR EACH $row->{action_orientation} $row->{action_statement};\n\n";
377         }
378
379         svn_check(
380             destdir   => $destdir.'/table',
381             fqn       => $fqtn,
382             ddl       => $ddl,
383             svn       => $svn,
384             to_add    => \@to_add,
385             to_commit => \@to_commit,
386         );
387     }
388
389     goto CLEANUP if not $GET_PROCS;
390
391 print "Got the DDL after " . elapsed_time() . " seconds.\n";
392    
393   GET_PROCS:
394     # FIXME: functions with the same name that take different arguments are allowed
395     # FIXME:   this is currently not taken into consideration
396     $functions_h = $dbh->prepare($functions) or die "died preparing: $DBI::errstr\n";
397     $functions_h->execute() or die "died executing: $DBI::errstr\n";
398
399     $getnumargs_h = $dbh->prepare($getnumargs) or die "died preparing: $DBI::errstr\n";
400     $funcargs_h = $dbh->prepare($funcargs) or die "died preparing: $DBI::errstr\n";
401     my %iomodes = (i => '', io => 'INOUT ', o => 'OUT ');
402     my @functions_found;
403
404   PROC_ROW:
405     while (my $proc_row = $functions_h->fetchrow_hashref()) {
406         my ($rschema, $rname) = ($proc_row->{routine_schema}, $proc_row->{routine_name});
407         my $fqfn = "$rschema.$rname";
408
409         # Add the proc name to the procs found, even if we don't end up
410         # processing it.
411         push(@procs_found, $fqfn);
412
413         next PROC_ROW if not $schema_check->($fqfn);
414         next if $fso->{suppress}->($fqfn);
415         $getnumargs_h->execute($rname, $rschema) or die "died executing: $DBI::errstr\n";
416         my $nums = $getnumargs_h->fetchrow_hashref();
417         $getnumargs_h->finish();
418         $proc_row->{args} = [];
419         my ($imin, $imax) = ($nums->{idx_min}, $nums->{idx_max});
420         if (defined $imax and $imax >= $imin) {
421             for my $i ($imin .. $imax) {
422                 $funcargs_h->execute($i, $rname, $rschema);
423                 push @{$proc_row->{args}}, $funcargs_h->fetchrow_hashref();
424                 $funcargs_h->finish();
425             }
426         }
427
428         my $proc = "CREATE OR REPLACE $proc_row->{routine_type} $rschema.$rname(";
429         $proc .= join(
430             ',', map(
431                 "$iomodes{$_->{iomode}}".($_->{argname}?"$_->{argname} ":'').$_->{typename},
432                 @{$proc_row->{args}}
433             )
434         );
435         $proc .= ")\nRETURNS ".($nums->{retset}?'setof ':'');
436         $proc .= ($proc_row->{data_type} eq 'USER-DEFINED'
437                    ? "$proc_row->{type_udt_schema}.$proc_row->{type_udt_name}"
438                    : $proc_row->{data_type});
439         $proc .= " AS \$\$$proc_row->{routine_definition}\$\$";
440         $proc .= " LANGUAGE '$proc_row->{external_language}';\n";
441         svn_check(
442             destdir   => $destdir.'/function',
443             fqn       => $fqfn,
444             ddl       => $proc,
445             svn       => $svn,
446             to_add    => \@to_add,
447             to_commit => \@to_commit,
448         );
449     }
450
451 print "Got the procedures after " . elapsed_time() . " seconds.\n";
452
453   CLEANUP:
454     # all done, go away
455     print "finished with $curhost, cleaning up...\n" if not $QUIET;
456     $tables_h->finish() if $tables_h;
457     $functions_h->finish() if $functions_h;
458     $dbh->disconnect();
459     print "done.\n" if not $QUIET;
460     $curhost = shift @hosts;
461 }
462
463 if ($DO_SVN) {
464     # FIXME: long cmd lines might be a problem eventually
465     my ($add, $commit, $addcmd, $commitcmd);
466
467     # svn commit -F /path/to/file
468     # so don't have to worry about shell escaping the commit msg
469     if ($default_fn) {
470         open CM, "> $fn" or die "couldn't write [$fn]: $!\n";
471         print CM $commit_msg;
472         close CM;
473     }
474
475     if (scalar @to_commit or scalar @to_add) {
476         if (scalar @to_add) {
477             $addcmd = "$svn add ".join(' ', @to_add);
478             print "The add command is:\n$addcmd\n\n" unless $QUIET;
479             $add = `$addcmd`;
480             print $add if not $QUIET;
481         }
482         $commitcmd = "$svn commit -F $fn $svnuser ".join(' ', @to_commit, @to_add);
483         # print $commitcmd . "\n";
484         $commit = `$commitcmd`;
485         print $commit if not $QUIET;
486     }
487
488     if ($do_svn_del) {
489         if (scalar(@tables_found) > 0 && scalar(@procs_found) > 0) {
490             my @delfiles = files_to_delete();
491
492             if (scalar(@delfiles)==0) {
493                 print "There are no files to delete from the SVN archive.\n";
494             }
495             else {
496            
497                 my $deletecmd = "$svn del $svnuser " . join(" ", @delfiles);
498                 print "The delete command is:\n$deletecmd\n\n" unless $QUIET;
499                 my $delete = `$deletecmd`;
500                 print $delete unless $QUIET;
501
502                 my $commcmd = "$svn commit -F $fn $svnuser " . join(" ", @delfiles);
503                 print "The commit command is:\n$commcmd\n\n" unless $QUIET;
504                 $commit = `$commcmd`;
505                 print $commit unless $QUIET;
506             }
507         }
508         else {
509             print STDERR "The list of present tables and procedure is incomplete.  We don't know for sure what to delete.\n";
510         }
511     }
512     unlink $fn if (-f $fn);
513 }
514
515 print "Cleaned up and finished after " . elapsed_time() . " seconds.\n";
516
517 QUIT: if (0) { }
518
519 END {
520     $tables_h->finish() if $tables_h;
521     $functions_h->finish() if $functions_h;
522     $dbh->disconnect() if $dbh;
523 }
524
525
526 # Get a list of the files on disk to remove from the SVN repository.
527 # The files represent either tables or procedures. If there is a file that was
528 # not found in the scan of tables and procedure then the table or procedure
529 # has been removed and the file also has to go.
530 sub files_to_delete {
531     # Double check that both tables and procedure were scanned. If tables,
532     # f'rinstance, were not scanned then all tables file would be deleted from
533     # the filesystem. We don't want that.
534     if (scalar(@tables_found) == 0 || scalar(@procs_found) == 0) {
535         print STDERR "The list of present tables and procedure is incomplete.  We don't know for sure what to delete.\n";
536         return undef;
537     }
538
539     # Make a hash of all the files representing the tables or procs on disk.
540     my %file_list;
541     my $dirh = DirHandle->new($destdir);
542     while (defined(my $d = $dirh->read())) {
543         $file_list{"$d"} = 1 if (-f "$destdir/$d" && $d =~ m/\.sql$/o);
544     }
545
546     # Go through the list of tables and procs found in the database and
547     # remove the corresponding entry from the file_list.
548     foreach my $f (@tables_found) {
549         delete($file_list{"$f.sql"});
550     }
551     foreach my $f (@procs_found) {
552         delete($file_list{"$f.sql"});
553     }
554
555     # The files that are left in the %file_list are those for which the table
556     # or procedure that they represent has been removed.
557     my @files = map { "$destdir/$_" } keys(%file_list);
558     return @files;
559 }
560
561 =head1 NAME
562
563 getddl - a ddl to svn script for postgres
564
565 =head1 SYNOPSIS
566
567 A perl script to query a postgres database, write schema to file, and then check in said files.
568
569 =head1 VERSION
570
571 This document refers to version 0.4 of getddl, released May 29, 2008
572
573 =head1 USAGE
574
575 To use getddl, you need to configure several variables inside the script (mostly having to do with different connection options). Once configured, you call gettdll at the command line.
576
577 Example 1: grab ddl for both the tables and function and dump it to /db/schema/ridley, check-in any modifications or new objects, and remove any entries that no longer exist in svn.
578
579     perl /home/postgres/getddl.pl --host ridley  --ddlbase /db/schema/ --getddl --getprocs --svn --svndel >>  /home/postgres/logs/getddl.log
580
581 Example 2: grab ddl of only database functions and dump it to /db/schema/kraid.
582
583     perl /home/postgres/getddl.pl --host kraid --ddlbase /db/schema --getprocs
584
585
586 =head1 BUGS AND LIMITATIONS
587
588 Overloaded functions will be dumped from the database non-deterministically
589
590 Some actions may not work on older versions of Postgres (before 8.1).
591
592 Please report any problems to robert@omniti.com.
593
594 =head1 TODO
595
596 =over
597
598 =item * clean up / optimize iteration for items in svn lists
599
600 =item * clean-up default hosts directives
601
602 =item * validate config options vs. command line options
603
604 =item * drop items into thier own directories (ie. host/tables/tblname.sql and host/functions/funcname.sql)
605
606 =item * add support for function overloading, preferrably dumping all versions to single file
607
608 =item * add support for other rcs systems
609
610 =back
611
612 =head1 LICENSE AND COPYRIGHT
613
614 Copyright (c) 2008 OmniTI, Inc.
615
616 Redistribution and use in source and binary forms, with or without
617 modification, are permitted provided that the following conditions are met:
618
619   1. Redistributions of source code must retain the above copyright notice,
620      this list of conditions and the following disclaimer.
621   2. Redistributions in binary form must reproduce the above copyright notice,
622      this list of conditions and the following disclaimer in the documentation
623      and/or other materials provided with the distribution.
624
625 THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
626 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
627 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
628 EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
629 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
630 OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
631 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
632 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
633 IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
634 OF SUCH DAMAGE.
635
636 =cut
637
638 # vim:ts=4:sw=4:et:is:
639
Note: See TracBrowser for help on using the browser.