Pass $options through directly instead of $generator
[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     my $field = shift;
251     return 1 if $field->data_type eq 'geometry';
252 }
253
254 sub is_geography {
255     my $field = shift;
256     return 1 if $field->data_type eq 'geography';
257 }
258
259 sub create_table
260 {
261     my ($table, $options) = @_;
262
263     my $generator = _generator($options);
264     my $no_comments = $options->{no_comments} || 0;
265     my $add_drop_table = $options->{add_drop_table} || 0;
266     my $postgres_version = $options->{postgres_version} || 0;
267     my $type_defs = $options->{type_defs} || {};
268
269     my $table_name = $table->name or next;
270     my $table_name_qt = $generator->quote($table_name);
271
272     my ( @comments, @field_defs, @index_defs, @constraint_defs, @fks );
273
274     push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
275
276     if ( !$no_comments and my $comments = $table->comments ) {
277         $comments =~ s/^/-- /mg;
278         push @comments, "-- Comments:\n$comments\n--\n";
279     }
280
281     #
282     # Fields
283     #
284     for my $field ( $table->get_fields ) {
285         push @field_defs, create_field($field, {
286             generator => $generator,
287             postgres_version => $postgres_version,
288             type_defs => $type_defs,
289             constraint_defs => \@constraint_defs,
290         });
291     }
292
293     #
294     # Index Declarations
295     #
296     for my $index ( $table->get_indices ) {
297         my ($idef, $constraints) = create_index($index, {
298             generator => $generator,
299         });
300         $idef and push @index_defs, $idef;
301         push @constraint_defs, @$constraints;
302     }
303
304     #
305     # Table constraints
306     #
307     for my $c ( $table->get_constraints ) {
308         my ($cdefs, $fks) = create_constraint($c, {
309             generator => $generator,
310         });
311         push @constraint_defs, @$cdefs;
312         push @fks, @$fks;
313     }
314
315
316     my $create_statement = join("\n", @comments);
317     if ($add_drop_table) {
318         if ($postgres_version >= 8.002) {
319             $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
320         } else {
321             $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
322         }
323     }
324     my $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
325     $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n" .
326                             join( ",\n", map { "  $_" } @field_defs, @constraint_defs ).
327                             "\n)"
328                             ;
329     $create_statement .= @index_defs ? ';' : q{};
330     $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
331         . join(";\n", @index_defs);
332
333     #
334     # Geometry
335     #
336     if (my @geometry_columns = grep { is_geometry($_) } $table->get_fields) {
337         $create_statement .= join(";\n", '', map{ drop_geometry_column($_, $options) } @geometry_columns) if $options->{add_drop_table};
338         $create_statement .= join(";\n", '', map{ add_geometry_column($_, $options) } @geometry_columns);
339     }
340
341     return $create_statement, \@fks;
342 }
343
344 sub create_view {
345     my ($view, $options) = @_;
346     my $generator = _generator($options);
347     my $postgres_version = $options->{postgres_version} || 0;
348     my $add_drop_view = $options->{add_drop_view};
349
350     my $view_name = $view->name;
351     debug("PKG: Looking at view '${view_name}'\n");
352
353     my $create = '';
354     $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
355         unless $options->{no_comments};
356     if ($add_drop_view) {
357         if ($postgres_version >= 8.002) {
358             $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
359         } else {
360             $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
361         }
362     }
363     $create .= 'CREATE';
364
365     my $extra = $view->extra;
366     $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
367     $create .= " VIEW " . $generator->quote($view_name);
368
369     if ( my @fields = $view->fields ) {
370         my $field_list = join ', ', map { $generator->quote($_) } @fields;
371         $create .= " ( ${field_list} )";
372     }
373
374     if ( my $sql = $view->sql ) {
375         $create .= " AS\n    ${sql}\n";
376     }
377
378     if ( $extra->{check_option} ) {
379         $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
380     }
381
382     return $create;
383 }
384
385 {
386
387     my %field_name_scope;
388
389     sub create_field
390     {
391         my ($field, $options) = @_;
392
393         my $generator = _generator($options);
394         my $table_name = $field->table->name;
395         my $constraint_defs = $options->{constraint_defs} || [];
396         my $postgres_version = $options->{postgres_version} || 0;
397         my $type_defs = $options->{type_defs} || {};
398
399         $field_name_scope{$table_name} ||= {};
400         my $field_name    = $field->name;
401         my $field_comments = '';
402         if (my $comments = $field->comments) {
403             $comments =~ s/(?<!\A)^/  -- /mg;
404             $field_comments = "-- $comments\n  ";
405         }
406
407         my $field_def     = $field_comments . $generator->quote($field_name);
408
409         #
410         # Datatype
411         #
412         my $data_type = lc $field->data_type;
413         my %extra     = $field->extra;
414         my $list      = $extra{'list'} || [];
415         my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
416
417         if ($postgres_version >= 8.003 && $data_type eq 'enum') {
418             my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
419             $field_def .= ' '. $type_name;
420             my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
421                                "CREATE TYPE $type_name AS ENUM ($commalist)";
422             if (! exists $type_defs->{$type_name} ) {
423                 $type_defs->{$type_name} = $new_type_def;
424             } elsif ( $type_defs->{$type_name} ne $new_type_def ) {
425                 die "Attempted to redefine type name '$type_name' as a different type.\n";
426             }
427         } else {
428             $field_def .= ' '. convert_datatype($field);
429         }
430
431         #
432         # Default value
433         #
434         __PACKAGE__->_apply_default_value(
435           $field,
436           \$field_def,
437           [
438             'NULL'              => \'NULL',
439             'now()'             => 'now()',
440             'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
441           ],
442         );
443
444         #
445         # Not null constraint
446         #
447         $field_def .= ' NOT NULL' unless $field->is_nullable;
448
449         #
450         # Geometry constraints
451         #
452         if (is_geometry($field)) {
453             foreach ( create_geometry_constraints($field, $options) ) {
454                 my ($cdefs, $fks) = create_constraint($_, $options);
455                 push @$constraint_defs, @$cdefs;
456                 push @$fks, @$fks;
457             }
458         }
459
460         return $field_def;
461     }
462 }
463
464 sub create_geometry_constraints {
465     my ($field, $options) = @_;
466
467     my $fname = _generator($options)->quote($field);
468     my @constraints;
469     push @constraints, SQL::Translator::Schema::Constraint->new(
470         name       => "enforce_dims_".$field->name,
471         expression => "(ST_NDims($fname) = ".$field->extra->{dimensions}.")",
472         table       => $field->table,
473         type       => CHECK_C,
474     );
475
476     push @constraints, SQL::Translator::Schema::Constraint->new(
477         name       => "enforce_srid_".$field->name,
478         expression => "(ST_SRID($fname) = ".$field->extra->{srid}.")",
479         table       => $field->table,
480         type       => CHECK_C,
481     );
482     push @constraints, SQL::Translator::Schema::Constraint->new(
483         name       => "enforce_geotype_".$field->name,
484         expression => "(GeometryType($fname) = ". __PACKAGE__->_quote_string($field->extra->{geometry_type}) ."::text OR $fname IS NULL)",
485         table       => $field->table,
486         type       => CHECK_C,
487     );
488
489     return @constraints;
490 }
491
492 {
493     my %index_name;
494     sub create_index
495     {
496         my ($index, $options) = @_;
497
498         my $generator = _generator($options);
499         my $table_name = $index->table->name;
500
501         my ($index_def, @constraint_defs);
502
503         my $name
504             = $index->name
505             || join('_', $table_name, 'idx', ++$index_name{ $table_name });
506
507         my $type = $index->type || NORMAL;
508         my @fields     =  $index->fields;
509         return unless @fields;
510
511         my $index_using;
512         my $index_where;
513         for my $opt ( $index->options ) {
514             if ( ref $opt eq 'HASH' ) {
515                 foreach my $key (keys %$opt) {
516                     my $value = $opt->{$key};
517                     next unless defined $value;
518                     if ( uc($key) eq 'USING' ) {
519                         $index_using = "USING $value";
520                     }
521                     elsif ( uc($key) eq 'WHERE' ) {
522                         $index_where = "WHERE $value";
523                     }
524                 }
525             }
526         }
527
528         my $def_start = 'CONSTRAINT ' . $generator->quote($name) . ' ';
529         my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
530         if ( $type eq PRIMARY_KEY ) {
531             push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
532         }
533         elsif ( $type eq UNIQUE ) {
534             push @constraint_defs, "${def_start}UNIQUE " .$field_names;
535         }
536         elsif ( $type eq NORMAL ) {
537             $index_def =
538                 'CREATE INDEX ' . $generator->quote($name) . ' on ' . $generator->quote($table_name) . ' ' .
539                 join ' ', grep { defined } $index_using, $field_names, $index_where;
540         }
541         else {
542             warn "Unknown index type ($type) on table $table_name.\n"
543                 if $WARN;
544         }
545
546         return $index_def, \@constraint_defs;
547     }
548 }
549
550 sub create_constraint
551 {
552     my ($c, $options) = @_;
553
554     my $generator = _generator($options);
555     my $table_name = $c->table->name;
556     my (@constraint_defs, @fks);
557
558     my $name = $c->name || '';
559
560     my @fields = grep { defined } $c->fields;
561
562     my @rfields = grep { defined } $c->reference_fields;
563
564     next if !@fields && $c->type ne CHECK_C;
565     my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
566     my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
567     if ( $c->type eq PRIMARY_KEY ) {
568         push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
569     }
570     elsif ( $c->type eq UNIQUE ) {
571         push @constraint_defs, "${def_start}UNIQUE " .$field_names;
572     }
573     elsif ( $c->type eq CHECK_C ) {
574         my $expression = $c->expression;
575         push @constraint_defs, "${def_start}CHECK ($expression)";
576     }
577     elsif ( $c->type eq FOREIGN_KEY ) {
578         my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
579             . "\n  REFERENCES " . $generator->quote($c->reference_table);
580
581         if ( @rfields ) {
582             $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
583         }
584
585         if ( $c->match_type ) {
586             $def .= ' MATCH ' .
587                 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
588         }
589
590         if ( $c->on_delete ) {
591             $def .= ' ON DELETE '. $c->on_delete;
592         }
593
594         if ( $c->on_update ) {
595             $def .= ' ON UPDATE '. $c->on_update;
596         }
597
598         if ( $c->deferrable ) {
599             $def .= ' DEFERRABLE';
600         }
601
602         push @fks, "$def";
603     }
604
605     return \@constraint_defs, \@fks;
606 }
607
608 sub create_trigger {
609   my ($trigger,$options) = @_;
610   my $generator = _generator($options);
611
612   my @statements;
613
614   push @statements, sprintf( 'DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name) )
615     if $options->{add_drop_trigger};
616
617   my $scope = $trigger->scope || '';
618   $scope = " FOR EACH $scope" if $scope;
619
620   push @statements, sprintf(
621     'CREATE TRIGGER %s %s %s ON %s%s %s',
622     $generator->quote($trigger->name),
623     $trigger->perform_action_when,
624     join( ' OR ', @{ $trigger->database_events } ),
625     $generator->quote($trigger->on_table),
626     $scope,
627     $trigger->action,
628   );
629
630   return @statements;
631 }
632
633 sub convert_datatype
634 {
635     my ($field) = @_;
636
637     my @size      = $field->size;
638     my $data_type = lc $field->data_type;
639     my $array = $data_type =~ s/\[\]$//;
640
641     if ( $data_type eq 'enum' ) {
642 #        my $len = 0;
643 #        $len = ($len < length($_)) ? length($_) : $len for (@$list);
644 #        my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
645 #        push @$constraint_defs,
646 #        'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
647 #           qq[IN ($commalist))];
648         $data_type = 'character varying';
649     }
650     elsif ( $data_type eq 'set' ) {
651         $data_type = 'character varying';
652     }
653     elsif ( $field->is_auto_increment ) {
654         if ( defined $size[0] && $size[0] > 11 ) {
655             $data_type = 'bigserial';
656         }
657         else {
658             $data_type = 'serial';
659         }
660         undef @size;
661     }
662     else {
663         $data_type  = defined $translate{ lc $data_type } ?
664             $translate{ lc $data_type } :
665             $data_type;
666     }
667
668     if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
669         if ( defined $size[0] && $size[0] > 6 ) {
670             $size[0] = 6;
671         }
672     }
673
674     if ( $data_type eq 'integer' ) {
675         if ( defined $size[0] && $size[0] > 0) {
676             if ( $size[0] > 10 ) {
677                 $data_type = 'bigint';
678             }
679             elsif ( $size[0] < 5 ) {
680                 $data_type = 'smallint';
681             }
682             else {
683                 $data_type = 'integer';
684             }
685         }
686         else {
687             $data_type = 'integer';
688         }
689     }
690
691     my $type_with_size = join('|',
692         'bit', 'varbit', 'character', 'bit varying', 'character varying',
693         'time', 'timestamp', 'interval', 'numeric', 'float'
694     );
695
696     if ( $data_type !~ /$type_with_size/ ) {
697         @size = ();
698     }
699
700     if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
701         $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
702         $data_type .= $2 if(defined $2);
703     } elsif ( defined $size[0] && $size[0] > 0 ) {
704         $data_type .= '(' . join( ',', @size ) . ')';
705     }
706     if($array)
707     {
708         $data_type .= '[]';
709     }
710
711     #
712     # Geography
713     #
714     if($data_type eq 'geography'){
715         $data_type .= '('.$field->extra->{geography_type}.','. $field->extra->{srid} .')'
716     }
717
718     return $data_type;
719 }
720
721
722 sub alter_field
723 {
724     my ($from_field, $to_field, $options) = @_;
725
726     die "Can't alter field in another table"
727         if($from_field->table->name ne $to_field->table->name);
728
729     my $generator = _generator($options);
730     my @out;
731
732     # drop geometry column and constraints
733     push @out,
734         drop_geometry_column($from_field, $options),
735         drop_geometry_constraints($from_field, $options),
736         if is_geometry($from_field);
737
738     # it's necessary to start with rename column cause this would affect
739     # all of the following statements which would be broken if do the
740     # rename later
741     # BUT: drop geometry is done before the rename, cause it work's on the
742     # $from_field directly
743     push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
744                        map($generator->quote($_),
745                            $to_field->table->name,
746                            $from_field->name,
747                            $to_field->name,
748                        ),
749                    )
750         if($from_field->name ne $to_field->name);
751
752     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
753                        map($generator->quote($_),
754                            $to_field->table->name,
755                            $to_field->name
756                        ),
757                    )
758         if(!$to_field->is_nullable and $from_field->is_nullable);
759
760     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
761                       map($generator->quote($_),
762                           $to_field->table->name,
763                           $to_field->name
764                       ),
765                    )
766        if (!$from_field->is_nullable and $to_field->is_nullable);
767
768
769     my $from_dt = convert_datatype($from_field);
770     my $to_dt   = convert_datatype($to_field);
771     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
772                        map($generator->quote($_),
773                            $to_field->table->name,
774                            $to_field->name
775                        ),
776                        $to_dt,
777                    )
778         if($to_dt ne $from_dt);
779
780     my $old_default = $from_field->default_value;
781     my $new_default = $to_field->default_value;
782     my $default_value = $to_field->default_value;
783
784     # fixes bug where output like this was created:
785     # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
786     if(ref $default_value eq "SCALAR" ) {
787         $default_value = $$default_value;
788     } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
789         $default_value = __PACKAGE__->_quote_string($default_value);
790     }
791
792     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
793                        map($generator->quote($_),
794                            $to_field->table->name,
795                            $to_field->name,
796                        ),
797                        $default_value,
798                    )
799         if ( defined $new_default &&
800              (!defined $old_default || $old_default ne $new_default) );
801
802     # fixes bug where removing the DEFAULT statement of a column
803     # would result in no change
804
805     push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
806                        map($generator->quote($_),
807                            $to_field->table->name,
808                            $to_field->name,
809                        ),
810                    )
811         if ( !defined $new_default && defined $old_default );
812
813     # add geometry column and constraints
814     push @out,
815         add_geometry_column($to_field, $options),
816         add_geometry_constraints($to_field, $options),
817         if is_geometry($to_field);
818
819     return wantarray ? @out : join(";\n", @out);
820 }
821
822 sub rename_field { alter_field(@_) }
823
824 sub add_field
825 {
826     my ($new_field,$options) = @_;
827
828     my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
829                       _generator($options)->quote($new_field->table->name),
830                       create_field($new_field, $options));
831     $out .= ";\n".add_geometry_column($new_field, $options)
832           . ";\n".add_geometry_constraints($new_field, $options)
833         if is_geometry($new_field);
834     return $out;
835
836 }
837
838 sub drop_field
839 {
840     my ($old_field, $options) = @_;
841
842     my $generator = _generator($options);
843
844     my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
845                       $generator->quote($old_field->table->name),
846                       $generator->quote($old_field->name));
847     $out .= ";\n".drop_geometry_column($old_field, $options)
848         if is_geometry($old_field);
849     return $out;
850 }
851
852 sub add_geometry_column {
853     my ($field, $options) = @_;
854
855     return sprintf(
856         "INSERT INTO geometry_columns VALUES (%s,%s,%s,%s,%s,%s,%s)",
857         map(__PACKAGE__->_quote_string($_),
858             '',
859             $field->table->schema->name,
860             $options->{table} ? $options->{table} : $field->table->name,
861             $field->name,
862             $field->extra->{dimensions},
863             $field->extra->{srid},
864             $field->extra->{geometry_type},
865         ),
866     );
867 }
868
869 sub drop_geometry_column {
870     my ($field) = @_;
871
872     return sprintf(
873         "DELETE FROM geometry_columns WHERE f_table_schema = %s AND f_table_name = %s AND f_geometry_column = %s",
874         map(__PACKAGE__->_quote_string($_),
875             $field->table->schema->name,
876             $field->table->name,
877             $field->name,
878         ),
879     );
880 }
881
882 sub add_geometry_constraints {
883     my ($field, $options) = @_;
884
885     return join(";\n", map { alter_create_constraint($_, $options) }
886                     create_geometry_constraints($field, $options));
887 }
888
889 sub drop_geometry_constraints {
890     my ($field, $options) = @_;
891
892     return join(";\n", map { alter_drop_constraint($_, $options) }
893                     create_geometry_constraints($field, $options));
894
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 = map {
913         drop_geometry_column($_, $options),
914         add_geometry_column($_, { %{$options}, table => $new_table }),
915     } grep { is_geometry($_) } $old_table->get_fields;
916
917     $options->{geometry_changes} = join (";\n",@geometry_changes) if @geometry_changes;
918
919     return alter_table($old_table, $options);
920 }
921
922 sub alter_create_index {
923     my ($index, $options) = @_;
924     my $generator = _generator($options);
925     my ($idef, $constraints) = create_index($index, $options);
926     return $index->type eq NORMAL ? $idef
927         : sprintf('ALTER TABLE %s ADD %s',
928               $generator->quote($index->table->name),
929               join(q{}, @$constraints)
930           );
931 }
932
933 sub alter_drop_index {
934     my ($index, $options) = @_;
935     return 'DROP INDEX '. _generator($options)->quote($index->name);
936 }
937
938 sub alter_drop_constraint {
939     my ($c, $options) = @_;
940     my $generator = _generator($options);
941
942     # attention: Postgres  has a very special naming structure for naming
943     # foreign keys and primary keys.  It names them using the name of the
944     # table as prefix and fkey or pkey as suffix, concatenated by an underscore
945     my $c_name;
946     if( $c->name ) {
947         # Already has a name, just use it
948         $c_name = $c->name;
949     } elsif ( $c->type eq FOREIGN_KEY ) {
950         # Doesn't have a name, and is foreign key, append '_fkey'
951         $c_name = $c->table->name . '_' . ($c->fields)[0] . '_fkey';
952     } elsif ( $c->type eq PRIMARY_KEY ) {
953         # Doesn't have a name, and is primary key, append '_pkey'
954         $c_name = $c->table->name . '_pkey';
955     }
956
957     return sprintf(
958         'ALTER TABLE %s DROP CONSTRAINT %s',
959         map { $generator->quote($_) } $c->table->name, $c_name,
960     );
961 }
962
963 sub alter_create_constraint {
964     my ($index, $options) = @_;
965     my $generator = _generator($options);
966     my ($defs, $fks) = create_constraint(@_);
967
968     # return if there are no constraint definitions so we don't run
969     # into output like this:
970     # ALTER TABLE users ADD ;
971
972     return unless(@{$defs} || @{$fks});
973     return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
974         : join( ' ', 'ALTER TABLE', $generator->quote($index->table->name),
975               'ADD', join(q{}, @{$defs}, @{$fks})
976           );
977 }
978
979 sub drop_table {
980     my ($table, $options) = @_;
981     my $generator = _generator($options);
982     my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
983
984     my @geometry_drops = map { drop_geometry_column($_); } grep { is_geometry($_) } $table->get_fields;
985
986     $out .= join(";\n", '', @geometry_drops) if @geometry_drops;
987     return $out;
988 }
989
990 sub batch_alter_table {
991   my ( $table, $diff_hash, $options ) = @_;
992
993   # as long as we're not renaming the table we don't need to be here
994   if ( @{$diff_hash->{rename_table}} == 0 ) {
995     return batch_alter_table_statements($diff_hash, $options);
996   }
997
998   # first we need to perform drops which are on old table
999   my @sql = batch_alter_table_statements($diff_hash, $options, qw(
1000     alter_drop_constraint
1001     alter_drop_index
1002     drop_field
1003   ));
1004
1005   # next comes the rename_table
1006   my $old_table = $diff_hash->{rename_table}[0][0];
1007   push @sql, rename_table( $old_table, $table, $options );
1008
1009   # for alter_field (and so also rename_field) we need to make sure old
1010   # field has table name set to new table otherwise calling alter_field dies
1011   $diff_hash->{alter_field} =
1012     [map { $_->[0]->table($table) && $_ } @{$diff_hash->{alter_field}}];
1013   $diff_hash->{rename_field} =
1014     [map { $_->[0]->table($table) && $_ } @{$diff_hash->{rename_field}}];
1015
1016   # now add everything else
1017   push @sql, batch_alter_table_statements($diff_hash, $options, qw(
1018     add_field
1019     alter_field
1020     rename_field
1021     alter_create_index
1022     alter_create_constraint
1023     alter_table
1024   ));
1025
1026   return @sql;
1027 }
1028
1029 1;
1030
1031 # -------------------------------------------------------------------
1032 # Life is full of misery, loneliness, and suffering --
1033 # and it's all over much too soon.
1034 # Woody Allen
1035 # -------------------------------------------------------------------
1036
1037 =pod
1038
1039 =head1 SEE ALSO
1040
1041 SQL::Translator, SQL::Translator::Producer::Oracle.
1042
1043 =head1 AUTHOR
1044
1045 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
1046
1047 =cut