9fb3e547b4a6f85422a919f8fe8a48b8c3a1a026
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / PostgreSQL.pm
1 package SQL::Translator::Producer::PostgreSQL;
2
3 =head1 NAME
4
5 SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
6
7 =head1 SYNOPSIS
8
9   my $t = SQL::Translator->new( parser => '...', producer => 'PostgreSQL' );
10   $t->translate;
11
12 =head1 DESCRIPTION
13
14 Creates a DDL suitable for PostgreSQL.  Very heavily based on the Oracle
15 producer.
16
17 Now handles PostGIS Geometry and Geography data types on table definitions.
18 Does not yet support PostGIS Views.
19
20 =cut
21
22 use strict;
23 use warnings;
24 our ( $DEBUG, $WARN );
25 our $VERSION = '1.59';
26 $DEBUG = 0 unless defined $DEBUG;
27
28 use base qw(SQL::Translator::Producer);
29 use SQL::Translator::Schema::Constants;
30 use SQL::Translator::Utils qw(debug header_comment parse_dbms_version batch_alter_table_statements normalize_quote_options);
31 use SQL::Translator::Generator::DDL::PostgreSQL;
32 use Data::Dumper;
33
34 use constant MAX_ID_LENGTH => 62;
35
36 {
37   my ($quoting_generator, $nonquoting_generator);
38   sub _generator {
39     my $options = shift;
40     return $options->{generator} if exists $options->{generator};
41
42     return normalize_quote_options($options)
43       ? $quoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new
44       : $nonquoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new(
45         quote_chars => [],
46       );
47   }
48 }
49
50 my ( %translate );
51
52 BEGIN {
53
54  %translate  = (
55     #
56     # MySQL types
57     #
58     double     => 'double precision',
59     decimal    => 'numeric',
60     int        => 'integer',
61     mediumint  => 'integer',
62     tinyint    => 'smallint',
63     char       => 'character',
64     varchar    => 'character varying',
65     longtext   => 'text',
66     mediumtext => 'text',
67     tinytext   => 'text',
68     tinyblob   => 'bytea',
69     blob       => 'bytea',
70     mediumblob => 'bytea',
71     longblob   => 'bytea',
72     enum       => 'character varying',
73     set        => 'character varying',
74     datetime   => 'timestamp',
75     year       => 'date',
76
77     #
78     # Oracle types
79     #
80     number     => 'integer',
81     varchar2   => 'character varying',
82     long       => 'text',
83     clob       => 'text',
84
85     #
86     # Sybase types
87     #
88     comment    => 'text',
89
90     #
91     # MS Access types
92     #
93     memo       => 'text',
94 );
95 }
96 my %truncated;
97
98 =pod
99
100 =head1 PostgreSQL Create Table Syntax
101
102   CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
103       { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
104       | table_constraint }  [, ... ]
105   )
106   [ INHERITS ( parent_table [, ... ] ) ]
107   [ WITH OIDS | WITHOUT OIDS ]
108
109 where column_constraint is:
110
111   [ CONSTRAINT constraint_name ]
112   { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
113     CHECK (expression) |
114     REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
115       [ ON DELETE action ] [ ON UPDATE action ] }
116   [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
117
118 and table_constraint is:
119
120   [ CONSTRAINT constraint_name ]
121   { UNIQUE ( column_name [, ... ] ) |
122     PRIMARY KEY ( column_name [, ... ] ) |
123     CHECK ( expression ) |
124     FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
125       [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
126   [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
127
128 =head1 Create Index Syntax
129
130   CREATE [ UNIQUE ] INDEX index_name ON table
131       [ USING acc_method ] ( column [ ops_name ] [, ...] )
132       [ WHERE predicate ]
133   CREATE [ UNIQUE ] INDEX index_name ON table
134       [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
135       [ WHERE predicate ]
136
137 =cut
138
139 sub produce {
140     my $translator       = shift;
141     local $DEBUG         = $translator->debug;
142     local $WARN          = $translator->show_warnings;
143     my $no_comments      = $translator->no_comments;
144     my $add_drop_table   = $translator->add_drop_table;
145     my $schema           = $translator->schema;
146     my $pargs            = $translator->producer_args;
147     my $postgres_version = parse_dbms_version(
148         $pargs->{postgres_version}, 'perl'
149     );
150
151     my $generator = _generator({ quote_identifiers => $translator->quote_identifiers });
152
153     my @output;
154     push @output, header_comment unless ($no_comments);
155
156     my (@table_defs, @fks);
157     my %type_defs;
158     for my $table ( $schema->get_tables ) {
159
160         my ($table_def, $fks) = create_table($table, {
161             generator         => $generator,
162             no_comments       => $no_comments,
163             postgres_version  => $postgres_version,
164             add_drop_table    => $add_drop_table,
165             type_defs         => \%type_defs,
166         });
167
168         push @table_defs, $table_def;
169         push @fks, @$fks;
170     }
171
172     for my $view ( $schema->get_views ) {
173         push @table_defs, create_view($view, {
174             postgres_version  => $postgres_version,
175             add_drop_view     => $add_drop_table,
176             generator         => $generator,
177             no_comments       => $no_comments,
178         });
179     }
180
181     for my $trigger ( $schema->get_triggers ) {
182       push @table_defs, create_trigger( $trigger, {
183           add_drop_trigger => $add_drop_table,
184           generator        => $generator,
185           no_comments      => $no_comments,
186         });
187     }
188
189     push @output, map { "$_;\n\n" } values %type_defs;
190     push @output, map { "$_;\n\n" } @table_defs;
191     if ( @fks ) {
192         push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
193         push @output, map { "$_;\n\n" } @fks;
194     }
195
196     if ( $WARN ) {
197         if ( %truncated ) {
198             warn "Truncated " . keys( %truncated ) . " names:\n";
199             warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
200         }
201     }
202
203     return wantarray
204         ? @output
205         : join ('', @output);
206 }
207
208 {
209     my %global_names;
210     sub mk_name {
211         my $basename      = shift || '';
212         my $type          = shift || '';
213         my $scope         = shift || '';
214         my $critical      = shift || '';
215         my $basename_orig = $basename;
216
217         my $max_name      = $type
218                             ? MAX_ID_LENGTH - (length($type) + 1)
219                             : MAX_ID_LENGTH;
220         $basename         = substr( $basename, 0, $max_name )
221                             if length( $basename ) > $max_name;
222         my $name          = $type ? "${type}_$basename" : $basename;
223
224         if ( $basename ne $basename_orig and $critical ) {
225             my $show_type = $type ? "+'$type'" : "";
226             warn "Truncating '$basename_orig'$show_type to ", MAX_ID_LENGTH,
227                 " character limit to make '$name'\n" if $WARN;
228             $truncated{ $basename_orig } = $name;
229         }
230
231         $scope ||= \%global_names;
232         if ( my $prev = $scope->{ $name } ) {
233             my $name_orig = $name;
234             $name        .= sprintf( "%02d", ++$prev );
235             substr($name, MAX_ID_LENGTH - 3) = "00"
236                 if length( $name ) > MAX_ID_LENGTH;
237
238             warn "The name '$name_orig' has been changed to ",
239                  "'$name' to make it unique.\n" if $WARN;
240
241             $scope->{ $name_orig }++;
242         }
243
244         $scope->{ $name }++;
245         return $name;
246     }
247 }
248
249 sub is_geometry
250 {
251    my $field = shift;
252    return 1 if $field->data_type eq 'geometry';
253 }
254
255 sub is_geography
256 {
257     my $field = shift;
258     return 1 if $field->data_type eq 'geography';
259 }
260
261 sub create_table
262 {
263     my ($table, $options) = @_;
264
265     my $generator = _generator($options);
266     my $no_comments = $options->{no_comments} || 0;
267     my $add_drop_table = $options->{add_drop_table} || 0;
268     my $postgres_version = $options->{postgres_version} || 0;
269     my $type_defs = $options->{type_defs} || {};
270
271     my $table_name = $table->name or next;
272     my $table_name_qt = $generator->quote($table_name);
273
274     my ( @comments, @field_defs, @index_defs, @constraint_defs, @fks );
275
276     push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
277
278     if ( !$no_comments and my $comments = $table->comments ) {
279         $comments =~ s/^/-- /mg;
280         push @comments, "-- Comments:\n$comments\n--\n";
281     }
282
283     #
284     # Fields
285     #
286     for my $field ( $table->get_fields ) {
287         push @field_defs, create_field($field, {
288             generator => $generator,
289             postgres_version => $postgres_version,
290             type_defs => $type_defs,
291             constraint_defs => \@constraint_defs,
292         });
293     }
294
295     #
296     # Index Declarations
297     #
298     for my $index ( $table->get_indices ) {
299         my ($idef, $constraints) = create_index($index, {
300             generator => $generator,
301         });
302         $idef and push @index_defs, $idef;
303         push @constraint_defs, @$constraints;
304     }
305
306     #
307     # Table constraints
308     #
309     for my $c ( $table->get_constraints ) {
310         my ($cdefs, $fks) = create_constraint($c, {
311             generator => $generator,
312         });
313         push @constraint_defs, @$cdefs;
314         push @fks, @$fks;
315     }
316
317
318     my $create_statement = join("\n", @comments);
319     if ($add_drop_table) {
320         if ($postgres_version >= 8.002) {
321             $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
322         } else {
323             $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
324         }
325     }
326     my $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
327     $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n" .
328                             join( ",\n", map { "  $_" } @field_defs, @constraint_defs ).
329                             "\n)"
330                             ;
331     $create_statement .= @index_defs ? ';' : q{};
332     $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
333         . join(";\n", @index_defs);
334
335    #
336    # Geometry
337    #
338    if(grep { is_geometry($_) } $table->get_fields){
339         $create_statement .= ";";
340         my @geometry_columns;
341         foreach my $col ($table->get_fields) { push(@geometry_columns,$col) if is_geometry($col); }
342       $create_statement .= "\n".join("\n", map{ drop_geometry_column($_) } @geometry_columns) if $options->{add_drop_table};
343       $create_statement .= "\n".join("\n", map{ add_geometry_column($_) } @geometry_columns);
344    }
345
346     return $create_statement, \@fks;
347 }
348
349 sub create_view {
350     my ($view, $options) = @_;
351     my $generator = _generator($options);
352     my $postgres_version = $options->{postgres_version} || 0;
353     my $add_drop_view = $options->{add_drop_view};
354
355     my $view_name = $view->name;
356     debug("PKG: Looking at view '${view_name}'\n");
357
358     my $create = '';
359     $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
360         unless $options->{no_comments};
361     if ($add_drop_view) {
362         if ($postgres_version >= 8.002) {
363             $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
364         } else {
365             $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
366         }
367     }
368     $create .= 'CREATE';
369
370     my $extra = $view->extra;
371     $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
372     $create .= " VIEW " . $generator->quote($view_name);
373
374     if ( my @fields = $view->fields ) {
375         my $field_list = join ', ', map { $generator->quote($_) } @fields;
376         $create .= " ( ${field_list} )";
377     }
378
379     if ( my $sql = $view->sql ) {
380         $create .= " AS\n    ${sql}\n";
381     }
382
383     if ( $extra->{check_option} ) {
384         $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
385     }
386
387     return $create;
388 }
389
390 {
391
392     my %field_name_scope;
393
394     sub create_field
395     {
396         my ($field, $options) = @_;
397
398         my $generator = _generator($options);
399         my $table_name = $field->table->name;
400         my $constraint_defs = $options->{constraint_defs} || [];
401         my $postgres_version = $options->{postgres_version} || 0;
402         my $type_defs = $options->{type_defs} || {};
403
404         $field_name_scope{$table_name} ||= {};
405         my $field_name    = $field->name;
406         my $field_comments = '';
407         if (my $comments = $field->comments) {
408             $comments =~ s/(?<!\A)^/  -- /mg;
409             $field_comments = "-- $comments\n  ";
410         }
411
412         my $field_def     = $field_comments . $generator->quote($field_name);
413
414         #
415         # Datatype
416         #
417         my $data_type = lc $field->data_type;
418         my %extra     = $field->extra;
419         my $list      = $extra{'list'} || [];
420         my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
421
422         if ($postgres_version >= 8.003 && $data_type eq 'enum') {
423             my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
424             $field_def .= ' '. $type_name;
425             my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
426                                "CREATE TYPE $type_name AS ENUM ($commalist)";
427             if (! exists $type_defs->{$type_name} ) {
428                 $type_defs->{$type_name} = $new_type_def;
429             } elsif ( $type_defs->{$type_name} ne $new_type_def ) {
430                 die "Attempted to redefine type name '$type_name' as a different type.\n";
431             }
432         } else {
433             $field_def .= ' '. convert_datatype($field);
434         }
435
436         #
437         # Default value
438         #
439         __PACKAGE__->_apply_default_value(
440           $field,
441           \$field_def,
442           [
443             'NULL'              => \'NULL',
444             'now()'             => 'now()',
445             'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
446           ],
447         );
448
449         #
450         # Not null constraint
451         #
452         $field_def .= ' NOT NULL' unless $field->is_nullable;
453
454       #
455       # Geometry constraints
456       #
457       if(is_geometry($field)){
458          foreach ( create_geometry_constraints($field) ) {
459             my ($cdefs, $fks) = create_constraint($_, {
460                 generator => $generator,
461             });
462             push @$constraint_defs, @$cdefs;
463             push @$fks, @$fks;
464          }
465         }
466
467         return $field_def;
468     }
469 }
470
471 sub create_geometry_constraints{
472    my $field = shift;
473
474    my @constraints;
475    push @constraints, SQL::Translator::Schema::Constraint->new(
476                      name       => "enforce_dims_".$field->name,
477                      expression => "(ST_NDims($field) = ".$field->extra->{dimensions}.")",
478                      table       => $field->table,
479                      type       => CHECK_C,
480                   );
481
482    push @constraints, SQL::Translator::Schema::Constraint->new(
483                      name       => "enforce_srid_".$field->name,
484                      expression => "(ST_SRID($field) = ".$field->extra->{srid}.")",
485                      table       => $field->table,
486                      type       => CHECK_C,
487                   );
488    push @constraints, SQL::Translator::Schema::Constraint->new(
489                      name       => "enforce_geotype_".$field->name,
490                      expression => "(GeometryType($field) = '".$field->extra->{geometry_type}."'::text OR $field IS NULL)",
491                      table       => $field->table,
492                      type       => CHECK_C,
493                   );
494
495    return @constraints;
496 }
497
498 {
499     my %index_name;
500     sub create_index
501     {
502         my ($index, $options) = @_;
503
504         my $generator = _generator($options);
505         my $table_name = $index->table->name;
506
507         my ($index_def, @constraint_defs);
508
509         my $name
510             = $index->name
511             || join('_', $table_name, 'idx', ++$index_name{ $table_name });
512
513         my $type = $index->type || NORMAL;
514         my @fields     =  $index->fields;
515         return unless @fields;
516
517         my $index_using;
518         my $index_where;
519         for my $opt ( $index->options ) {
520             if ( ref $opt eq 'HASH' ) {
521                 foreach my $key (keys %$opt) {
522                     my $value = $opt->{$key};
523                     next unless defined $value;
524                     if ( uc($key) eq 'USING' ) {
525                         $index_using = "USING $value";
526                     }
527                     elsif ( uc($key) eq 'WHERE' ) {
528                         $index_where = "WHERE $value";
529                     }
530                 }
531             }
532         }
533
534         my $def_start = 'CONSTRAINT ' . $generator->quote($name) . ' ';
535         my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
536         if ( $type eq PRIMARY_KEY ) {
537             push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
538         }
539         elsif ( $type eq UNIQUE ) {
540             push @constraint_defs, "${def_start}UNIQUE " .$field_names;
541         }
542         elsif ( $type eq NORMAL ) {
543             $index_def =
544                 'CREATE INDEX ' . $generator->quote($name) . ' on ' . $generator->quote($table_name) . ' ' .
545                 join ' ', grep { defined } $index_using, $field_names, $index_where;
546         }
547         else {
548             warn "Unknown index type ($type) on table $table_name.\n"
549                 if $WARN;
550         }
551
552         return $index_def, \@constraint_defs;
553     }
554 }
555
556 sub create_constraint
557 {
558     my ($c, $options) = @_;
559
560     my $generator = _generator($options);
561     my $table_name = $c->table->name;
562     my (@constraint_defs, @fks);
563
564     my $name = $c->name || '';
565
566     my @fields = grep { defined } $c->fields;
567
568     my @rfields = grep { defined } $c->reference_fields;
569
570     next if !@fields && $c->type ne CHECK_C;
571     my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
572     my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
573     if ( $c->type eq PRIMARY_KEY ) {
574         push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
575     }
576     elsif ( $c->type eq UNIQUE ) {
577         push @constraint_defs, "${def_start}UNIQUE " .$field_names;
578     }
579     elsif ( $c->type eq CHECK_C ) {
580         my $expression = $c->expression;
581         push @constraint_defs, "${def_start}CHECK ($expression)";
582     }
583     elsif ( $c->type eq FOREIGN_KEY ) {
584         my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
585             . "\n  REFERENCES " . $generator->quote($c->reference_table);
586
587         if ( @rfields ) {
588             $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
589         }
590
591         if ( $c->match_type ) {
592             $def .= ' MATCH ' .
593                 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
594         }
595
596         if ( $c->on_delete ) {
597             $def .= ' ON DELETE '. $c->on_delete;
598         }
599
600         if ( $c->on_update ) {
601             $def .= ' ON UPDATE '. $c->on_update;
602         }
603
604         if ( $c->deferrable ) {
605             $def .= ' DEFERRABLE';
606         }
607
608         push @fks, "$def";
609     }
610
611     return \@constraint_defs, \@fks;
612 }
613
614 sub create_trigger {
615   my ($trigger,$options) = @_;
616   my $generator = _generator($options);
617
618   my @statements;
619
620   push @statements, sprintf( 'DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name) )
621     if $options->{add_drop_trigger};
622
623   my $scope = $trigger->scope || '';
624   $scope = " FOR EACH $scope" if $scope;
625
626   push @statements, sprintf(
627     'CREATE TRIGGER %s %s %s ON %s%s %s',
628     $generator->quote($trigger->name),
629     $trigger->perform_action_when,
630     join( ' OR ', @{ $trigger->database_events } ),
631     $generator->quote($trigger->on_table),
632     $scope,
633     $trigger->action,
634   );
635
636   return @statements;
637 }
638
639 sub convert_datatype
640 {
641     my ($field) = @_;
642
643     my @size      = $field->size;
644     my $data_type = lc $field->data_type;
645     my $array = $data_type =~ s/\[\]$//;
646
647     if ( $data_type eq 'enum' ) {
648 #        my $len = 0;
649 #        $len = ($len < length($_)) ? length($_) : $len for (@$list);
650 #        my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
651 #        push @$constraint_defs,
652 #        'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
653 #           qq[IN ($commalist))];
654         $data_type = 'character varying';
655     }
656     elsif ( $data_type eq 'set' ) {
657         $data_type = 'character varying';
658     }
659     elsif ( $field->is_auto_increment ) {
660         if ( defined $size[0] && $size[0] > 11 ) {
661             $data_type = 'bigserial';
662         }
663         else {
664             $data_type = 'serial';
665         }
666         undef @size;
667     }
668     else {
669         $data_type  = defined $translate{ lc $data_type } ?
670             $translate{ lc $data_type } :
671             $data_type;
672     }
673
674     if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
675         if ( defined $size[0] && $size[0] > 6 ) {
676             $size[0] = 6;
677         }
678     }
679
680     if ( $data_type eq 'integer' ) {
681         if ( defined $size[0] && $size[0] > 0) {
682             if ( $size[0] > 10 ) {
683                 $data_type = 'bigint';
684             }
685             elsif ( $size[0] < 5 ) {
686                 $data_type = 'smallint';
687             }
688             else {
689                 $data_type = 'integer';
690             }
691         }
692         else {
693             $data_type = 'integer';
694         }
695     }
696
697     my $type_with_size = join('|',
698         'bit', 'varbit', 'character', 'bit varying', 'character varying',
699         'time', 'timestamp', 'interval', 'numeric', 'float'
700     );
701
702     if ( $data_type !~ /$type_with_size/ ) {
703         @size = ();
704     }
705
706     if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
707         $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
708         $data_type .= $2 if(defined $2);
709     } elsif ( defined $size[0] && $size[0] > 0 ) {
710         $data_type .= '(' . join( ',', @size ) . ')';
711     }
712     if($array)
713     {
714         $data_type .= '[]';
715     }
716
717     #
718     # Geography
719     #
720     if($data_type eq 'geography'){
721         $data_type .= '('.$field->extra->{geography_type}.','. $field->extra->{srid} .')'
722     }
723
724     return $data_type;
725 }
726
727
728 sub alter_field
729 {
730     my ($from_field, $to_field, $options) = @_;
731
732     die "Can't alter field in another table"
733         if($from_field->table->name ne $to_field->table->name);
734
735     my $generator = _generator($options);
736     my @out;
737
738     # drop geometry column and constraints
739     push @out, drop_geometry_column($from_field) if is_geometry($from_field);
740     push @out, drop_geometry_constraints($from_field) if is_geometry($from_field);
741
742     # it's necessary to start with rename column cause this would affect
743     # all of the following statements which would be broken if do the
744     # rename later
745     # BUT: drop geometry is done before the rename, cause it work's on the
746     # $from_field directly
747     push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
748                        map($generator->quote($_),
749                            $to_field->table->name,
750                            $from_field->name,
751                            $to_field->name,
752                        ),
753                    )
754         if($from_field->name ne $to_field->name);
755
756     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
757                        map($generator->quote($_),
758                            $to_field->table->name,
759                            $to_field->name
760                        ),
761                    )
762         if(!$to_field->is_nullable and $from_field->is_nullable);
763
764     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
765                       map($generator->quote($_),
766                           $to_field->table->name,
767                           $to_field->name
768                       ),
769                    )
770        if (!$from_field->is_nullable and $to_field->is_nullable);
771
772
773     my $from_dt = convert_datatype($from_field);
774     my $to_dt   = convert_datatype($to_field);
775     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
776                        map($generator->quote($_),
777                            $to_field->table->name,
778                            $to_field->name
779                        ),
780                        $to_dt,
781                    )
782         if($to_dt ne $from_dt);
783
784     my $old_default = $from_field->default_value;
785     my $new_default = $to_field->default_value;
786     my $default_value = $to_field->default_value;
787
788     # fixes bug where output like this was created:
789     # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
790     if(ref $default_value eq "SCALAR" ) {
791         $default_value = $$default_value;
792     } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
793         $default_value = __PACKAGE__->_quote_string($default_value);
794     }
795
796     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
797                        map($generator->quote($_),
798                            $to_field->table->name,
799                            $to_field->name,
800                        ),
801                        $default_value,
802                    )
803         if ( defined $new_default &&
804              (!defined $old_default || $old_default ne $new_default) );
805
806     # fixes bug where removing the DEFAULT statement of a column
807     # would result in no change
808
809     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
810                        map($generator->quote($_),
811                            $to_field->table->name,
812                            $to_field->name,
813                        ),
814                    )
815         if ( !defined $new_default && defined $old_default );
816
817     # add geometry column and constraints
818     push @out, add_geometry_column($to_field) if is_geometry($to_field);
819     push @out, add_geometry_constraints($to_field) if is_geometry($to_field);
820
821     return wantarray ? @out : join(";\n", @out);
822 }
823
824 sub rename_field { alter_field(@_) }
825
826 sub add_field
827 {
828     my ($new_field,$options) = @_;
829
830     my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
831                       _generator($options)->quote($new_field->table->name),
832                       create_field($new_field, $options));
833     $out .= "\n".add_geometry_column($new_field) if is_geometry($new_field);
834     $out .= "\n".add_geometry_constraints($new_field) if is_geometry($new_field);
835     return $out;
836
837 }
838
839 sub drop_field
840 {
841     my ($old_field, $options) = @_;
842
843     my $generator = _generator($options);
844
845     my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
846                       $generator->quote($old_field->table->name),
847                       $generator->quote($old_field->name));
848         $out .= "\n".drop_geometry_column($old_field) if is_geometry($old_field);
849     return $out;
850 }
851
852 sub add_geometry_column{
853    my ($field,$options) = @_;
854
855    my $out = sprintf("INSERT INTO geometry_columns VALUES ('%s','%s','%s','%s','%s','%s','%s')",
856                   '',
857                   $field->table->schema->name,
858                   $options->{table} ? $options->{table} : $field->table->name,
859                   $field->name,
860                   $field->extra->{dimensions},
861                   $field->extra->{srid},
862                   $field->extra->{geometry_type});
863     return $out;
864 }
865
866 sub drop_geometry_column
867 {
868    my $field = shift;
869
870    my $out = sprintf("DELETE FROM geometry_columns WHERE f_table_schema = '%s' AND f_table_name = '%s' AND f_geometry_column = '%s'",
871                   $field->table->schema->name,
872                   $field->table->name,
873                   $field->name);
874     return $out;
875 }
876
877 sub add_geometry_constraints{
878    my $field = shift;
879
880    my @constraints = create_geometry_constraints($field);
881
882    my $out = join("\n", map { alter_create_constraint($_); } @constraints);
883
884    return $out;
885 }
886
887 sub drop_geometry_constraints{
888    my $field = shift;
889
890    my @constraints = create_geometry_constraints($field);
891
892    my $out = join("\n", map { alter_drop_constraint($_); } @constraints);
893
894    return $out;
895 }
896
897 sub alter_table {
898     my ($to_table, $options) = @_;
899     my $generator = _generator($options);
900     my $out = sprintf('ALTER TABLE %s %s',
901                       $generator->quote($to_table->name),
902                       $options->{alter_table_action});
903     $out .= "\n".$options->{geometry_changes} if $options->{geometry_changes};
904     return $out;
905 }
906
907 sub rename_table {
908     my ($old_table, $new_table, $options) = @_;
909     my $generator = _generator($options);
910     $options->{alter_table_action} = "RENAME TO " . $generator->quote($new_table);
911
912    my @geometry_changes;
913    push @geometry_changes, map { drop_geometry_column($_); } grep { is_geometry($_) } $old_table->get_fields;
914    push @geometry_changes, map { add_geometry_column($_, { table => $new_table }); } grep { is_geometry($_) } $old_table->get_fields;
915
916     $options->{geometry_changes} = join ("\n",@geometry_changes) if scalar(@geometry_changes);
917
918     return alter_table($old_table, $options);
919 }
920
921 sub alter_create_index {
922     my ($index, $options) = @_;
923     my $generator = _generator($options);
924     my ($idef, $constraints) = create_index($index, {
925         generator => $generator,
926     });
927     return $index->type eq NORMAL ? $idef
928         : sprintf('ALTER TABLE %s ADD %s',
929               $generator->quote($index->table->name),
930               join(q{}, @$constraints)
931           );
932 }
933
934 sub alter_drop_index {
935     my ($index, $options) = @_;
936     return 'DROP INDEX '. _generator($options)->quote($index->name);
937 }
938
939 sub alter_drop_constraint {
940     my ($c, $options) = @_;
941     my $generator = _generator($options);
942
943     # attention: Postgres  has a very special naming structure for naming
944     # foreign keys and primary keys.  It names them using the name of the
945     # table as prefix and fkey or pkey as suffix, concatenated by an underscore
946     my $c_name;
947     if( $c->name ) {
948         # Already has a name, just use it
949         $c_name = $c->name;
950     } elsif ( $c->type eq FOREIGN_KEY ) {
951         # Doesn't have a name, and is foreign key, append '_fkey'
952         $c_name = $c->table->name . '_' . ($c->fields)[0] . '_fkey';
953     } elsif ( $c->type eq PRIMARY_KEY ) {
954         # Doesn't have a name, and is primary key, append '_pkey'
955         $c_name = $c->table->name . '_pkey';
956     }
957
958     return sprintf(
959         'ALTER TABLE %s DROP CONSTRAINT %s',
960         map { $generator->quote($_) } $c->table->name, $c_name,
961     );
962 }
963
964 sub alter_create_constraint {
965     my ($index, $options) = @_;
966     my $generator = _generator($options);
967     my ($defs, $fks) = create_constraint(@_);
968
969     # return if there are no constraint definitions so we don't run
970     # into output like this:
971     # ALTER TABLE users ADD ;
972
973     return unless(@{$defs} || @{$fks});
974     return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
975         : join( ' ', 'ALTER TABLE', $generator->quote($index->table->name),
976               'ADD', join(q{}, @{$defs}, @{$fks})
977           );
978 }
979
980 sub drop_table {
981     my ($table, $options) = @_;
982     my $generator = _generator($options);
983     my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
984
985     my @geometry_drops = map { drop_geometry_column($_); } grep { is_geometry($_) } $table->get_fields;
986
987     $out .= "\n".join("\n",@geometry_drops) if scalar(@geometry_drops);
988     return $out;
989 }
990
991 sub batch_alter_table {
992   my ( $table, $diff_hash, $options ) = @_;
993
994   # as long as we're not renaming the table we don't need to be here
995   if ( @{$diff_hash->{rename_table}} == 0 ) {
996     return batch_alter_table_statements($diff_hash, $options);
997   }
998
999   # first we need to perform drops which are on old table
1000   my @sql = batch_alter_table_statements($diff_hash, $options, qw(
1001     alter_drop_constraint
1002     alter_drop_index
1003     drop_field
1004   ));
1005
1006   # next comes the rename_table
1007   my $old_table = $diff_hash->{rename_table}[0][0];
1008   push @sql, rename_table( $old_table, $table, $options );
1009
1010   # for alter_field (and so also rename_field) we need to make sure old
1011   # field has table name set to new table otherwise calling alter_field dies
1012   $diff_hash->{alter_field} =
1013     [map { $_->[0]->table($table) && $_ } @{$diff_hash->{alter_field}}];
1014   $diff_hash->{rename_field} =
1015     [map { $_->[0]->table($table) && $_ } @{$diff_hash->{rename_field}}];
1016
1017   # now add everything else
1018   push @sql, batch_alter_table_statements($diff_hash, $options, qw(
1019     add_field
1020     alter_field
1021     rename_field
1022     alter_create_index
1023     alter_create_constraint
1024     alter_table
1025   ));
1026
1027   return @sql;
1028 }
1029
1030 1;
1031
1032 # -------------------------------------------------------------------
1033 # Life is full of misery, loneliness, and suffering --
1034 # and it's all over much too soon.
1035 # Woody Allen
1036 # -------------------------------------------------------------------
1037
1038 =pod
1039
1040 =head1 SEE ALSO
1041
1042 SQL::Translator, SQL::Translator::Producer::Oracle.
1043
1044 =head1 AUTHOR
1045
1046 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
1047
1048 =cut