1 package SQL::Translator::Producer::PostgreSQL;
3 # -------------------------------------------------------------------
4 # Copyright (C) 2002-2009 SQLFairy Authors
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; version 2.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
19 # -------------------------------------------------------------------
23 SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
27 my $t = SQL::Translator->new( parser => '...', producer => 'PostgreSQL' );
32 Creates a DDL suitable for PostgreSQL. Very heavily based on the Oracle
39 use vars qw[ $DEBUG $WARN $VERSION %used_names ];
41 $DEBUG = 0 unless defined $DEBUG;
43 use base qw(SQL::Translator::Producer);
44 use SQL::Translator::Schema::Constants;
45 use SQL::Translator::Utils qw(debug header_comment);
62 mediumint => 'integer',
63 smallint => 'smallint',
64 tinyint => 'smallint',
66 varchar => 'character varying',
73 mediumblob => 'bytea',
75 enum => 'character varying',
76 set => 'character varying',
78 datetime => 'timestamp',
80 timestamp => 'timestamp',
88 varchar2 => 'character varying',
98 varchar => 'character varying',
99 datetime => 'timestamp',
104 tinyint => 'smallint',
110 my %reserved = map { $_, 1 } qw[
111 ALL ANALYSE ANALYZE AND ANY AS ASC
113 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
114 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
115 DEFAULT DEFERRABLE DESC DISTINCT DO
117 FALSE FOR FOREIGN FREEZE FROM FULL
119 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
120 JOIN LEADING LEFT LIKE LIMIT
121 NATURAL NEW NOT NOTNULL NULL
122 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
123 PRIMARY PUBLIC REFERENCES RIGHT
124 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
125 UNION UNIQUE USER USING VERBOSE WHEN WHERE
128 # my $max_id_length = 62;
129 my %used_identifiers = ();
136 =head1 PostgreSQL Create Table Syntax
138 CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
139 { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
140 | table_constraint } [, ... ]
142 [ INHERITS ( parent_table [, ... ] ) ]
143 [ WITH OIDS | WITHOUT OIDS ]
145 where column_constraint is:
147 [ CONSTRAINT constraint_name ]
148 { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
150 REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
151 [ ON DELETE action ] [ ON UPDATE action ] }
152 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
154 and table_constraint is:
156 [ CONSTRAINT constraint_name ]
157 { UNIQUE ( column_name [, ... ] ) |
158 PRIMARY KEY ( column_name [, ... ] ) |
159 CHECK ( expression ) |
160 FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
161 [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
162 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
164 =head1 Create Index Syntax
166 CREATE [ UNIQUE ] INDEX index_name ON table
167 [ USING acc_method ] ( column [ ops_name ] [, ...] )
169 CREATE [ UNIQUE ] INDEX index_name ON table
170 [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
175 # -------------------------------------------------------------------
177 my $translator = shift;
178 local $DEBUG = $translator->debug;
179 local $WARN = $translator->show_warnings;
180 my $no_comments = $translator->no_comments;
181 my $add_drop_table = $translator->add_drop_table;
182 my $schema = $translator->schema;
183 my $pargs = $translator->producer_args;
184 local %used_names = ();
186 my $postgres_version = $pargs->{postgres_version} || 0;
189 $qt = '"' if ($translator->quote_table_names);
191 $qf = '"' if ($translator->quote_field_names);
194 push @output, header_comment unless ($no_comments);
196 my (@table_defs, @fks);
197 for my $table ( $schema->get_tables ) {
199 my ($table_def, $fks) = create_table($table,
200 { quote_table_names => $qt,
201 quote_field_names => $qf,
202 no_comments => $no_comments,
203 postgres_version => $postgres_version,
204 add_drop_table => $add_drop_table,});
205 push @table_defs, $table_def;
210 for my $view ( $schema->get_views ) {
211 push @table_defs, create_view($view, {
212 add_drop_view => $add_drop_table,
213 quote_table_names => $qt,
214 quote_field_names => $qf,
215 no_comments => $no_comments,
219 push @output, map { "$_;\n\n" } @table_defs;
221 push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
222 push @output, map { "$_;\n\n" } @fks;
227 warn "Truncated " . keys( %truncated ) . " names:\n";
228 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
232 warn "Encounted " . keys( %unreserve ) .
233 " unsafe names in schema (reserved or invalid):\n";
234 warn "\t" . join( "\n\t", sort keys %unreserve ) . "\n";
240 : join ('', @output);
243 # -------------------------------------------------------------------
245 my $basename = shift || '';
246 my $type = shift || '';
247 my $scope = shift || '';
248 my $critical = shift || '';
249 my $basename_orig = $basename;
250 # my $max_id_length = 62;
252 ? $max_id_length - (length($type) + 1)
254 $basename = substr( $basename, 0, $max_name )
255 if length( $basename ) > $max_name;
256 my $name = $type ? "${type}_$basename" : $basename;
258 if ( $basename ne $basename_orig and $critical ) {
259 my $show_type = $type ? "+'$type'" : "";
260 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
261 "character limit to make '$name'\n" if $WARN;
262 $truncated{ $basename_orig } = $name;
265 $scope ||= \%global_names;
266 if ( my $prev = $scope->{ $name } ) {
267 my $name_orig = $name;
268 $name .= sprintf( "%02d", ++$prev );
269 substr($name, $max_id_length - 3) = "00"
270 if length( $name ) > $max_id_length;
272 warn "The name '$name_orig' has been changed to ",
273 "'$name' to make it unique.\n" if $WARN;
275 $scope->{ $name_orig }++;
282 # -------------------------------------------------------------------
284 my $name = shift || '';
285 my $schema_obj_name = shift || '';
287 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
289 # also trap fields that don't begin with a letter
290 return $name if (!$reserved{ uc $name }) && $name =~ /^[a-z]/i;
292 if ( $schema_obj_name ) {
293 ++$unreserve{"$schema_obj_name.$name"};
296 ++$unreserve{"$name (table name)"};
299 my $unreserve = sprintf '%s_', $name;
300 return $unreserve.$suffix;
303 # -------------------------------------------------------------------
304 sub next_unused_name {
305 my $name = shift || '';
306 if ( !defined( $used_names{$name} ) ) {
307 $used_names{$name} = $name;
312 while ( defined( $used_names{ $name . $i } ) ) {
316 $used_names{$name} = $name;
323 my ($table, $options) = @_;
325 my $qt = $options->{quote_table_names} || '';
326 my $qf = $options->{quote_field_names} || '';
327 my $no_comments = $options->{no_comments} || 0;
328 my $add_drop_table = $options->{add_drop_table} || 0;
329 my $postgres_version = $options->{postgres_version} || 0;
331 my $table_name = $table->name or next;
332 my ( $fql_tbl_name ) = ( $table_name =~ s/\W(.*)$// ) ? $1 : q{};
333 my $table_name_ur = $qt ? $table_name
334 : $fql_tbl_name ? join('.', $table_name, unreserve($fql_tbl_name))
335 : unreserve($table_name);
336 $table->name($table_name_ur);
338 # print STDERR "$table_name table_name\n";
339 my ( @comments, @field_defs, @sequence_defs, @constraint_defs, @type_defs, @type_drops, @fks );
341 push @comments, "--\n-- Table: $table_name_ur\n--\n" unless $no_comments;
343 if ( $table->comments and !$no_comments ){
344 my $c = "-- Comments: \n-- ";
345 $c .= join "\n-- ", $table->comments;
353 my %field_name_scope;
354 for my $field ( $table->get_fields ) {
355 push @field_defs, create_field($field, { quote_table_names => $qt,
356 quote_field_names => $qf,
357 table_name => $table_name_ur,
358 postgres_version => $postgres_version,
359 type_defs => \@type_defs,
360 type_drops => \@type_drops,
361 constraint_defs => \@constraint_defs,});
368 # my $idx_name_default;
369 for my $index ( $table->get_indices ) {
370 my ($idef, $constraints) = create_index($index,
372 quote_field_names => $qf,
373 quote_table_names => $qt,
374 table_name => $table_name,
376 $idef and push @index_defs, $idef;
377 push @constraint_defs, @$constraints;
384 for my $c ( $table->get_constraints ) {
385 my ($cdefs, $fks) = create_constraint($c,
387 quote_field_names => $qf,
388 quote_table_names => $qt,
389 table_name => $table_name,
391 push @constraint_defs, @$cdefs;
398 if(exists $table->{extra}{temporary}) {
399 $temporary = $table->{extra}{temporary} ? "TEMPORARY " : "";
402 my $create_statement;
403 $create_statement = join("\n", @comments);
404 if ($add_drop_table) {
405 if ($postgres_version >= 8.2) {
406 $create_statement .= qq[DROP TABLE IF EXISTS $qt$table_name_ur$qt CASCADE;\n];
407 $create_statement .= join (";\n", @type_drops) . ";\n"
408 if $postgres_version >= 8.3 && scalar @type_drops;
410 $create_statement .= qq[DROP TABLE $qt$table_name_ur$qt CASCADE;\n];
413 $create_statement .= join(";\n", @type_defs) . ";\n"
414 if $postgres_version >= 8.3 && scalar @type_defs;
415 $create_statement .= qq[CREATE ${temporary}TABLE $qt$table_name_ur$qt (\n].
416 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
419 $create_statement .= @index_defs ? ';' : q{};
420 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
421 . join(";\n", @index_defs);
423 return $create_statement, \@fks;
427 my ($view, $options) = @_;
428 my $qt = $options->{quote_table_names} || '';
429 my $qf = $options->{quote_field_names} || '';
430 my $add_drop_view = $options->{add_drop_view};
432 my $view_name = $view->name;
433 debug("PKG: Looking at view '${view_name}'\n");
436 $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n"
437 unless $options->{no_comments};
438 $create .= "DROP VIEW ${qt}${view_name}${qt};\n" if $add_drop_view;
441 my $extra = $view->extra;
442 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
443 $create .= " VIEW ${qt}${view_name}${qt}";
445 if ( my @fields = $view->fields ) {
446 my $field_list = join ', ', map { "${qf}${_}${qf}" } @fields;
447 $create .= " ( ${field_list} )";
450 if ( my $sql = $view->sql ) {
451 $create .= " AS\n ${sql}\n";
454 if ( $extra->{check_option} ) {
455 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
463 my %field_name_scope;
467 my ($field, $options) = @_;
469 my $qt = $options->{quote_table_names} || '';
470 my $qf = $options->{quote_field_names} || '';
471 my $table_name = $field->table->name;
472 my $constraint_defs = $options->{constraint_defs} || [];
473 my $postgres_version = $options->{postgres_version} || 0;
474 my $type_defs = $options->{type_defs} || [];
475 my $type_drops = $options->{type_drops} || [];
477 $field_name_scope{$table_name} ||= {};
478 my $field_name = $field->name;
479 my $field_name_ur = $qf ? $field_name : unreserve($field_name, $table_name );
480 $field->name($field_name_ur);
481 my $field_comments = $field->comments
482 ? "-- " . $field->comments . "\n "
485 my $field_def = $field_comments.qq[$qf$field_name_ur$qf];
490 my @size = $field->size;
491 my $data_type = lc $field->data_type;
492 my %extra = $field->extra;
493 my $list = $extra{'list'} || [];
494 # todo deal with embedded quotes
495 my $commalist = join( ', ', map { qq['$_'] } @$list );
497 if ($postgres_version >= 8.3 && $field->data_type eq 'enum') {
498 my $type_name = $field->table->name . '_' . $field->name . '_type';
499 $field_def .= ' '. $type_name;
500 push @$type_defs, "CREATE TYPE $type_name AS ENUM ($commalist)";
501 push @$type_drops, "DROP TYPE IF EXISTS $type_name";
503 $field_def .= ' '. convert_datatype($field);
509 my $default = $field->default_value;
510 if ( defined $default ) {
511 SQL::Translator::Producer->_apply_default_value(
517 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
523 # Not null constraint
525 $field_def .= ' NOT NULL' unless $field->is_nullable;
533 my ($index, $options) = @_;
535 my $qt = $options->{quote_table_names} ||'';
536 my $qf = $options->{quote_field_names} ||'';
537 my $table_name = $index->table->name;
538 # my $table_name_ur = $qt ? unreserve($table_name) : $table_name;
540 my ($index_def, @constraint_defs);
542 my $name = $index->name || '';
544 $name = next_unused_name($name);
547 my $type = $index->type || NORMAL;
549 map { $_ =~ s/\(.+\)//; $_ }
550 map { $qt ? $_ : unreserve($_, $table_name ) }
554 my $def_start = qq[CONSTRAINT "$name" ];
555 if ( $type eq PRIMARY_KEY ) {
556 push @constraint_defs, "${def_start}PRIMARY KEY ".
557 '(' .$qf . join( $qf. ', '.$qf, @fields ) . $qf . ')';
559 elsif ( $type eq UNIQUE ) {
560 push @constraint_defs, "${def_start}UNIQUE " .
561 '(' . $qf . join( $qf. ', '.$qf, @fields ) . $qf.')';
563 elsif ( $type eq NORMAL ) {
565 "CREATE INDEX ${qf}${name}${qf} on ${qt}${table_name}${qt} (".
566 join( ', ', map { qq[$qf$_$qf] } @fields ).
571 warn "Unknown index type ($type) on table $table_name.\n"
575 return $index_def, \@constraint_defs;
578 sub create_constraint
580 my ($c, $options) = @_;
582 my $qf = $options->{quote_field_names} ||'';
583 my $qt = $options->{quote_table_names} ||'';
584 my $table_name = $c->table->name;
585 my (@constraint_defs, @fks);
587 my $name = $c->name || '';
589 $name = next_unused_name($name);
593 map { $_ =~ s/\(.+\)//; $_ }
594 map { $qt ? $_ : unreserve( $_, $table_name )}
598 map { $_ =~ s/\(.+\)//; $_ }
599 map { $qt ? $_ : unreserve( $_, $table_name )}
600 $c->reference_fields;
602 next if !@fields && $c->type ne CHECK_C;
603 my $def_start = $name ? qq[CONSTRAINT "$name" ] : '';
604 if ( $c->type eq PRIMARY_KEY ) {
605 push @constraint_defs, "${def_start}PRIMARY KEY ".
606 '('.$qf . join( $qf.', '.$qf, @fields ) . $qf.')';
608 elsif ( $c->type eq UNIQUE ) {
609 $name = next_unused_name($name);
610 push @constraint_defs, "${def_start}UNIQUE " .
611 '('.$qf . join( $qf.', '.$qf, @fields ) . $qf.')';
613 elsif ( $c->type eq CHECK_C ) {
614 my $expression = $c->expression;
615 push @constraint_defs, "${def_start}CHECK ($expression)";
617 elsif ( $c->type eq FOREIGN_KEY ) {
618 my $def .= "ALTER TABLE ${qt}${table_name}${qt} ADD FOREIGN KEY (" .
619 join( ', ', map { qq[$qf$_$qf] } @fields ) . ')' .
620 "\n REFERENCES " . $qt . $c->reference_table . $qt;
623 $def .= ' ('.$qf . join( $qf.', '.$qf, @rfields ) . $qf.')';
626 if ( $c->match_type ) {
628 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
631 if ( $c->on_delete ) {
632 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
635 if ( $c->on_update ) {
636 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
639 if ( $c->deferrable ) {
640 $def .= ' DEFERRABLE';
646 return \@constraint_defs, \@fks;
653 my @size = $field->size;
654 my $data_type = lc $field->data_type;
656 if ( $data_type eq 'enum' ) {
658 # $len = ($len < length($_)) ? length($_) : $len for (@$list);
659 # my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
660 # push @$constraint_defs,
661 # qq[CONSTRAINT "$chk_name" CHECK ($qf$field_name$qf ].
662 # qq[IN ($commalist))];
663 $data_type = 'character varying';
665 elsif ( $data_type eq 'set' ) {
666 $data_type = 'character varying';
668 elsif ( $field->is_auto_increment ) {
669 if ( defined $size[0] && $size[0] > 11 ) {
670 $data_type = 'bigserial';
673 $data_type = 'serial';
678 $data_type = defined $translate{ $data_type } ?
679 $translate{ $data_type } :
683 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
684 if ( defined $size[0] && $size[0] > 6 ) {
689 if ( $data_type eq 'integer' ) {
690 if ( defined $size[0] && $size[0] > 0) {
691 if ( $size[0] > 10 ) {
692 $data_type = 'bigint';
694 elsif ( $size[0] < 5 ) {
695 $data_type = 'smallint';
698 $data_type = 'integer';
702 $data_type = 'integer';
705 my @type_without_size = qw/bigint boolean box bytea cidr circle date inet
706 integer smallint text line lseg macaddr money
707 path point polygon real/;
708 foreach (@type_without_size) {
709 if ( $data_type =~ qr/$_/ ) {
714 if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
715 $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
716 $data_type .= $2 if(defined $2);
717 } elsif ( defined $size[0] && $size[0] > 0 ) {
718 $data_type .= '(' . join( ',', @size ) . ')';
729 my ($from_field, $to_field) = @_;
731 die "Can't alter field in another table"
732 if($from_field->table->name ne $to_field->table->name);
735 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
736 $to_field->table->name,
737 $to_field->name) if(!$to_field->is_nullable and
738 $from_field->is_nullable);
740 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
741 $to_field->table->name,
743 if ( !$from_field->is_nullable and $to_field->is_nullable );
746 my $from_dt = convert_datatype($from_field);
747 my $to_dt = convert_datatype($to_field);
748 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
749 $to_field->table->name,
751 $to_dt) if($to_dt ne $from_dt);
753 push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
754 $to_field->table->name,
756 $to_field->name) if($from_field->name ne $to_field->name);
758 my $old_default = $from_field->default_value;
759 my $new_default = $to_field->default_value;
760 my $default_value = $to_field->default_value;
762 # fixes bug where output like this was created:
763 # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
764 if(ref $default_value eq "SCALAR" ) {
765 $default_value = $$default_value;
766 } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
767 $default_value =~ s/'/''/xsmg;
768 $default_value = q(') . $default_value . q(');
771 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
772 $to_field->table->name,
775 if ( defined $new_default &&
776 (!defined $old_default || $old_default ne $new_default) );
778 # fixes bug where removing the DEFAULT statement of a column
779 # would result in no change
781 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
782 $to_field->table->name,
784 if ( !defined $new_default && defined $old_default );
787 return wantarray ? @out : join("\n", @out);
790 sub rename_field { alter_field(@_) }
794 my ($new_field) = @_;
796 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
797 $new_field->table->name,
798 create_field($new_field));
805 my ($old_field) = @_;
807 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
808 $old_field->table->name,
815 my ($to_table, $options) = @_;
816 my $qt = $options->{quote_table_names} || '';
817 my $out = sprintf('ALTER TABLE %s %s',
818 $qt . $to_table->name . $qt,
819 $options->{alter_table_action});
824 my ($old_table, $new_table, $options) = @_;
825 my $qt = $options->{quote_table_names} || '';
826 $options->{alter_table_action} = "RENAME TO $qt$new_table$qt";
827 return alter_table($old_table, $options);
830 sub alter_create_index {
831 my ($index, $options) = @_;
832 my $qt = $options->{quote_table_names} || '';
833 my $qf = $options->{quote_field_names} || '';
834 my ($idef, $constraints) = create_index($index, {
835 quote_field_names => $qf,
836 quote_table_names => $qt,
837 table_name => $index->table->name,
839 return $index->type eq NORMAL ? $idef
840 : sprintf('ALTER TABLE %s ADD %s',
841 $qt . $index->table->name . $qt,
842 join(q{}, @$constraints)
846 sub alter_drop_index {
847 my ($index, $options) = @_;
848 my $index_name = $index->name;
849 return "DROP INDEX $index_name";
852 sub alter_drop_constraint {
853 my ($c, $options) = @_;
854 my $qt = $options->{quote_table_names} || '';
855 my $qc = $options->{quote_field_names} || '';
856 my $out = sprintf('ALTER TABLE %s DROP CONSTRAINT %s',
857 $qt . $c->table->name . $qt,
858 $qc . $c->name . $qc );
862 sub alter_create_constraint {
863 my ($index, $options) = @_;
864 my $qt = $options->{quote_table_names} || '';
865 my ($defs, $fks) = create_constraint(@_);
867 # return if there are no constraint definitions so we don't run
868 # into output like this:
869 # ALTER TABLE users ADD ;
871 return unless(@{$defs} || @{$fks});
872 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
873 : join( ' ', 'ALTER TABLE', $qt.$index->table->name.$qt,
874 'ADD', join(q{}, @{$defs}, @{$fks})
879 my ($table, $options) = @_;
880 my $qt = $options->{quote_table_names} || '';
881 return "DROP TABLE $qt$table$qt CASCADE";
886 # -------------------------------------------------------------------
887 # Life is full of misery, loneliness, and suffering --
888 # and it's all over much too soon.
890 # -------------------------------------------------------------------
896 SQL::Translator, SQL::Translator::Producer::Oracle.
900 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.