root/trunk/getddl/getddl.pl

Revision 13, 23.3 kB (checked in by robert, 6 years ago)

v0.4, adds usage information into the docs

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