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);
48 my ( %translate, %index_name );
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 my $postgres_version = $pargs->{postgres_version} || 0;
186 my $qt = $translator->quote_table_names ? q{"} : q{};
187 my $qf = $translator->quote_field_names ? q{"} : q{};
190 push @output, header_comment unless ($no_comments);
192 my (@table_defs, @fks);
193 for my $table ( $schema->get_tables ) {
195 my ($table_def, $fks) = create_table($table, {
196 quote_table_names => $qt,
197 quote_field_names => $qf,
198 no_comments => $no_comments,
199 postgres_version => $postgres_version,
200 add_drop_table => $add_drop_table,
203 push @table_defs, $table_def;
207 for my $view ( $schema->get_views ) {
208 push @table_defs, create_view($view, {
209 add_drop_view => $add_drop_table,
210 quote_table_names => $qt,
211 quote_field_names => $qf,
212 no_comments => $no_comments,
216 push @output, map { "$_;\n\n" } @table_defs;
218 push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
219 push @output, map { "$_;\n\n" } @fks;
224 warn "Truncated " . keys( %truncated ) . " names:\n";
225 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
229 warn "Encounted " . keys( %unreserve ) .
230 " unsafe names in schema (reserved or invalid):\n";
231 warn "\t" . join( "\n\t", sort keys %unreserve ) . "\n";
237 : join ('', @output);
240 # -------------------------------------------------------------------
242 my $basename = shift || '';
243 my $type = shift || '';
244 my $scope = shift || '';
245 my $critical = shift || '';
246 my $basename_orig = $basename;
247 # my $max_id_length = 62;
249 ? $max_id_length - (length($type) + 1)
251 $basename = substr( $basename, 0, $max_name )
252 if length( $basename ) > $max_name;
253 my $name = $type ? "${type}_$basename" : $basename;
255 if ( $basename ne $basename_orig and $critical ) {
256 my $show_type = $type ? "+'$type'" : "";
257 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
258 "character limit to make '$name'\n" if $WARN;
259 $truncated{ $basename_orig } = $name;
262 $scope ||= \%global_names;
263 if ( my $prev = $scope->{ $name } ) {
264 my $name_orig = $name;
265 $name .= sprintf( "%02d", ++$prev );
266 substr($name, $max_id_length - 3) = "00"
267 if length( $name ) > $max_id_length;
269 warn "The name '$name_orig' has been changed to ",
270 "'$name' to make it unique.\n" if $WARN;
272 $scope->{ $name_orig }++;
279 # -------------------------------------------------------------------
281 my $name = shift || '';
282 my $schema_obj_name = shift || '';
284 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
286 # also trap fields that don't begin with a letter
287 return $name if (!$reserved{ uc $name }) && $name =~ /^[a-z]/i;
289 if ( $schema_obj_name ) {
290 ++$unreserve{"$schema_obj_name.$name"};
293 ++$unreserve{"$name (table name)"};
296 my $unreserve = sprintf '%s_', $name;
297 return $unreserve.$suffix;
300 # -------------------------------------------------------------------
301 sub next_unused_name {
302 my $orig_name = shift or return;
303 my $name = $orig_name;
305 my $suffix_gen = sub {
307 return ++$suffix ? '' : $suffix;
311 $name = $orig_name . $suffix_gen->();
312 last if $used_names{ $name }++;
320 my ($table, $options) = @_;
322 my $qt = $options->{quote_table_names} || '';
323 my $qf = $options->{quote_field_names} || '';
324 my $no_comments = $options->{no_comments} || 0;
325 my $add_drop_table = $options->{add_drop_table} || 0;
326 my $postgres_version = $options->{postgres_version} || 0;
328 my $table_name = $table->name or next;
329 my ( $fql_tbl_name ) = ( $table_name =~ s/\W(.*)$// ) ? $1 : q{};
330 my $table_name_ur = $qt ? $table_name
331 : $fql_tbl_name ? join('.', $table_name, unreserve($fql_tbl_name))
332 : unreserve($table_name);
333 $table->name($table_name_ur);
335 # print STDERR "$table_name table_name\n";
336 my ( @comments, @field_defs, @sequence_defs, @constraint_defs, @type_defs, @type_drops, @fks );
338 push @comments, "--\n-- Table: $table_name_ur\n--\n" unless $no_comments;
340 if ( $table->comments and !$no_comments ){
341 my $c = "-- Comments: \n-- ";
342 $c .= join "\n-- ", $table->comments;
350 my %field_name_scope;
351 for my $field ( $table->get_fields ) {
352 push @field_defs, create_field($field, { quote_table_names => $qt,
353 quote_field_names => $qf,
354 table_name => $table_name_ur,
355 postgres_version => $postgres_version,
356 type_defs => \@type_defs,
357 type_drops => \@type_drops,
358 constraint_defs => \@constraint_defs,});
365 # my $idx_name_default;
366 for my $index ( $table->get_indices ) {
367 my ($idef, $constraints) = create_index($index,
369 quote_field_names => $qf,
370 quote_table_names => $qt,
371 table_name => $table_name,
373 $idef and push @index_defs, $idef;
374 push @constraint_defs, @$constraints;
381 for my $c ( $table->get_constraints ) {
382 my ($cdefs, $fks) = create_constraint($c,
384 quote_field_names => $qf,
385 quote_table_names => $qt,
386 table_name => $table_name,
388 push @constraint_defs, @$cdefs;
395 if(exists $table->{extra}{temporary}) {
396 $temporary = $table->{extra}{temporary} ? "TEMPORARY " : "";
399 my $create_statement;
400 $create_statement = join("\n", @comments);
401 if ($add_drop_table) {
402 if ($postgres_version >= 8.2) {
403 $create_statement .= qq[DROP TABLE IF EXISTS $qt$table_name_ur$qt CASCADE;\n];
404 $create_statement .= join (";\n", @type_drops) . ";\n"
405 if $postgres_version >= 8.3 && scalar @type_drops;
407 $create_statement .= qq[DROP TABLE $qt$table_name_ur$qt CASCADE;\n];
410 $create_statement .= join(";\n", @type_defs) . ";\n"
411 if $postgres_version >= 8.3 && scalar @type_defs;
412 $create_statement .= qq[CREATE ${temporary}TABLE $qt$table_name_ur$qt (\n].
413 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
416 $create_statement .= @index_defs ? ';' : q{};
417 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
418 . join(";\n", @index_defs);
420 return $create_statement, \@fks;
424 my ($view, $options) = @_;
425 my $qt = $options->{quote_table_names} || '';
426 my $qf = $options->{quote_field_names} || '';
427 my $add_drop_view = $options->{add_drop_view};
429 my $view_name = $view->name;
430 debug("PKG: Looking at view '${view_name}'\n");
433 $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n"
434 unless $options->{no_comments};
435 $create .= "DROP VIEW ${qt}${view_name}${qt};\n" if $add_drop_view;
438 my $extra = $view->extra;
439 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
440 $create .= " VIEW ${qt}${view_name}${qt}";
442 if ( my @fields = $view->fields ) {
443 my $field_list = join ', ', map { "${qf}${_}${qf}" } @fields;
444 $create .= " ( ${field_list} )";
447 if ( my $sql = $view->sql ) {
448 $create .= " AS\n ${sql}\n";
451 if ( $extra->{check_option} ) {
452 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
460 my %field_name_scope;
464 my ($field, $options) = @_;
466 my $qt = $options->{quote_table_names} || '';
467 my $qf = $options->{quote_field_names} || '';
468 my $table_name = $field->table->name;
469 my $constraint_defs = $options->{constraint_defs} || [];
470 my $postgres_version = $options->{postgres_version} || 0;
471 my $type_defs = $options->{type_defs} || [];
472 my $type_drops = $options->{type_drops} || [];
474 $field_name_scope{$table_name} ||= {};
475 my $field_name = $field->name;
476 my $field_name_ur = $qf ? $field_name : unreserve($field_name, $table_name );
477 $field->name($field_name_ur);
478 my $field_comments = $field->comments
479 ? "-- " . $field->comments . "\n "
482 my $field_def = $field_comments.qq[$qf$field_name_ur$qf];
487 my @size = $field->size;
488 my $data_type = lc $field->data_type;
489 my %extra = $field->extra;
490 my $list = $extra{'list'} || [];
491 # todo deal with embedded quotes
492 my $commalist = join( ', ', map { qq['$_'] } @$list );
494 if ($postgres_version >= 8.3 && $field->data_type eq 'enum') {
495 my $type_name = $field->table->name . '_' . $field->name . '_type';
496 $field_def .= ' '. $type_name;
497 push @$type_defs, "CREATE TYPE $type_name AS ENUM ($commalist)";
498 push @$type_drops, "DROP TYPE IF EXISTS $type_name";
500 $field_def .= ' '. convert_datatype($field);
506 my $default = $field->default_value;
507 if ( defined $default ) {
508 SQL::Translator::Producer->_apply_default_value(
514 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
520 # Not null constraint
522 $field_def .= ' NOT NULL' unless $field->is_nullable;
530 my ($index, $options) = @_;
532 my $qt = $options->{quote_table_names} ||'';
533 my $qf = $options->{quote_field_names} ||'';
534 my $table_name = $index->table->name;
535 # my $table_name_ur = $qt ? unreserve($table_name) : $table_name;
537 my ($index_def, @constraint_defs);
539 my $name = next_unused_name(
541 || join('_', $table_name, 'idx', ++$index_name{ $table_name })
544 my $type = $index->type || NORMAL;
546 map { $_ =~ s/\(.+\)//; $_ }
547 map { $qt ? $_ : unreserve($_, $table_name ) }
551 my $def_start = qq[CONSTRAINT "$name" ];
552 if ( $type eq PRIMARY_KEY ) {
553 push @constraint_defs, "${def_start}PRIMARY KEY ".
554 '(' .$qf . join( $qf. ', '.$qf, @fields ) . $qf . ')';
556 elsif ( $type eq UNIQUE ) {
557 push @constraint_defs, "${def_start}UNIQUE " .
558 '(' . $qf . join( $qf. ', '.$qf, @fields ) . $qf.')';
560 elsif ( $type eq NORMAL ) {
562 "CREATE INDEX ${qf}${name}${qf} on ${qt}${table_name}${qt} (".
563 join( ', ', map { qq[$qf$_$qf] } @fields ).
568 warn "Unknown index type ($type) on table $table_name.\n"
572 return $index_def, \@constraint_defs;
575 sub create_constraint
577 my ($c, $options) = @_;
579 my $qf = $options->{quote_field_names} ||'';
580 my $qt = $options->{quote_table_names} ||'';
581 my $table_name = $c->table->name;
582 my (@constraint_defs, @fks);
584 my $name = $c->name || '';
586 $name = next_unused_name($name);
590 map { $_ =~ s/\(.+\)//; $_ }
591 map { $qt ? $_ : unreserve( $_, $table_name )}
595 map { $_ =~ s/\(.+\)//; $_ }
596 map { $qt ? $_ : unreserve( $_, $table_name )}
597 $c->reference_fields;
599 next if !@fields && $c->type ne CHECK_C;
600 my $def_start = $name ? qq[CONSTRAINT "$name" ] : '';
601 if ( $c->type eq PRIMARY_KEY ) {
602 push @constraint_defs, "${def_start}PRIMARY KEY ".
603 '('.$qf . join( $qf.', '.$qf, @fields ) . $qf.')';
605 elsif ( $c->type eq UNIQUE ) {
606 $name = next_unused_name($name);
607 push @constraint_defs, "${def_start}UNIQUE " .
608 '('.$qf . join( $qf.', '.$qf, @fields ) . $qf.')';
610 elsif ( $c->type eq CHECK_C ) {
611 my $expression = $c->expression;
612 push @constraint_defs, "${def_start}CHECK ($expression)";
614 elsif ( $c->type eq FOREIGN_KEY ) {
615 my $def .= "ALTER TABLE ${qt}${table_name}${qt} ADD FOREIGN KEY (" .
616 join( ', ', map { qq[$qf$_$qf] } @fields ) . ')' .
617 "\n REFERENCES " . $qt . $c->reference_table . $qt;
620 $def .= ' ('.$qf . join( $qf.', '.$qf, @rfields ) . $qf.')';
623 if ( $c->match_type ) {
625 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
628 if ( $c->on_delete ) {
629 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
632 if ( $c->on_update ) {
633 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
636 if ( $c->deferrable ) {
637 $def .= ' DEFERRABLE';
643 return \@constraint_defs, \@fks;
650 my @size = $field->size;
651 my $data_type = lc $field->data_type;
653 if ( $data_type eq 'enum' ) {
655 # $len = ($len < length($_)) ? length($_) : $len for (@$list);
656 # my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
657 # push @$constraint_defs,
658 # qq[CONSTRAINT "$chk_name" CHECK ($qf$field_name$qf ].
659 # qq[IN ($commalist))];
660 $data_type = 'character varying';
662 elsif ( $data_type eq 'set' ) {
663 $data_type = 'character varying';
665 elsif ( $field->is_auto_increment ) {
666 if ( defined $size[0] && $size[0] > 11 ) {
667 $data_type = 'bigserial';
670 $data_type = 'serial';
675 $data_type = defined $translate{ $data_type } ?
676 $translate{ $data_type } :
680 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
681 if ( defined $size[0] && $size[0] > 6 ) {
686 if ( $data_type eq 'integer' ) {
687 if ( defined $size[0] && $size[0] > 0) {
688 if ( $size[0] > 10 ) {
689 $data_type = 'bigint';
691 elsif ( $size[0] < 5 ) {
692 $data_type = 'smallint';
695 $data_type = 'integer';
699 $data_type = 'integer';
703 my $type_with_size = join('|',
704 'bit', 'varbit', 'character', 'bit varying', 'character varying',
705 'time', 'timestamp', 'interval', 'numeric'
708 if ( $data_type !~ /$type_with_size/ ) {
712 if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
713 $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
714 $data_type .= $2 if(defined $2);
715 } elsif ( defined $size[0] && $size[0] > 0 ) {
716 $data_type .= '(' . join( ',', @size ) . ')';
725 my ($from_field, $to_field) = @_;
727 die "Can't alter field in another table"
728 if($from_field->table->name ne $to_field->table->name);
731 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
732 $to_field->table->name,
733 $to_field->name) if(!$to_field->is_nullable and
734 $from_field->is_nullable);
736 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
737 $to_field->table->name,
739 if ( !$from_field->is_nullable and $to_field->is_nullable );
742 my $from_dt = convert_datatype($from_field);
743 my $to_dt = convert_datatype($to_field);
744 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
745 $to_field->table->name,
747 $to_dt) if($to_dt ne $from_dt);
749 push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
750 $to_field->table->name,
752 $to_field->name) if($from_field->name ne $to_field->name);
754 my $old_default = $from_field->default_value;
755 my $new_default = $to_field->default_value;
756 my $default_value = $to_field->default_value;
758 # fixes bug where output like this was created:
759 # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
760 if(ref $default_value eq "SCALAR" ) {
761 $default_value = $$default_value;
762 } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
763 $default_value =~ s/'/''/xsmg;
764 $default_value = q(') . $default_value . q(');
767 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
768 $to_field->table->name,
771 if ( defined $new_default &&
772 (!defined $old_default || $old_default ne $new_default) );
774 # fixes bug where removing the DEFAULT statement of a column
775 # would result in no change
777 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
778 $to_field->table->name,
780 if ( !defined $new_default && defined $old_default );
783 return wantarray ? @out : join("\n", @out);
786 sub rename_field { alter_field(@_) }
790 my ($new_field) = @_;
792 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
793 $new_field->table->name,
794 create_field($new_field));
801 my ($old_field) = @_;
803 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
804 $old_field->table->name,
811 my ($to_table, $options) = @_;
812 my $qt = $options->{quote_table_names} || '';
813 my $out = sprintf('ALTER TABLE %s %s',
814 $qt . $to_table->name . $qt,
815 $options->{alter_table_action});
820 my ($old_table, $new_table, $options) = @_;
821 my $qt = $options->{quote_table_names} || '';
822 $options->{alter_table_action} = "RENAME TO $qt$new_table$qt";
823 return alter_table($old_table, $options);
826 sub alter_create_index {
827 my ($index, $options) = @_;
828 my $qt = $options->{quote_table_names} || '';
829 my $qf = $options->{quote_field_names} || '';
830 my ($idef, $constraints) = create_index($index, {
831 quote_field_names => $qf,
832 quote_table_names => $qt,
833 table_name => $index->table->name,
835 return $index->type eq NORMAL ? $idef
836 : sprintf('ALTER TABLE %s ADD %s',
837 $qt . $index->table->name . $qt,
838 join(q{}, @$constraints)
842 sub alter_drop_index {
843 my ($index, $options) = @_;
844 my $index_name = $index->name;
845 return "DROP INDEX $index_name";
848 sub alter_drop_constraint {
849 my ($c, $options) = @_;
850 my $qt = $options->{quote_table_names} || '';
851 my $qc = $options->{quote_field_names} || '';
852 my $out = sprintf('ALTER TABLE %s DROP CONSTRAINT %s',
853 $qt . $c->table->name . $qt,
854 $qc . $c->name . $qc );
858 sub alter_create_constraint {
859 my ($index, $options) = @_;
860 my $qt = $options->{quote_table_names} || '';
861 my ($defs, $fks) = create_constraint(@_);
863 # return if there are no constraint definitions so we don't run
864 # into output like this:
865 # ALTER TABLE users ADD ;
867 return unless(@{$defs} || @{$fks});
868 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
869 : join( ' ', 'ALTER TABLE', $qt.$index->table->name.$qt,
870 'ADD', join(q{}, @{$defs}, @{$fks})
875 my ($table, $options) = @_;
876 my $qt = $options->{quote_table_names} || '';
877 return "DROP TABLE $qt$table$qt CASCADE";
882 # -------------------------------------------------------------------
883 # Life is full of misery, loneliness, and suffering --
884 # and it's all over much too soon.
886 # -------------------------------------------------------------------
892 SQL::Translator, SQL::Translator::Producer::Oracle.
896 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.