Remove or use unused variables in PostgreSQL producer
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / PostgreSQL.pm
CommitLineData
f8f0253c 1package SQL::Translator::Producer::PostgreSQL;
2
96844cae 3=head1 NAME
4
5SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
6
20770e44 7=head1 SYNOPSIS
8
9 my $t = SQL::Translator->new( parser => '...', producer => 'PostgreSQL' );
10 $t->translate;
11
12=head1 DESCRIPTION
13
14Creates a DDL suitable for PostgreSQL. Very heavily based on the Oracle
15producer.
16
e83ad71c 17Now handles PostGIS Geometry and Geography data types on table definitions.
18Does not yet support PostGIS Views.
ea93df61 19
96844cae 20=cut
21
f8f0253c 22use strict;
bfb5a568 23use warnings;
0c04c5a2 24our ( $DEBUG, $WARN );
25our $VERSION = '1.59';
a25ac5d2 26$DEBUG = 0 unless defined $DEBUG;
f8f0253c 27
8d11f4cb 28use base qw(SQL::Translator::Producer);
0c43e0a1 29use SQL::Translator::Schema::Constants;
f82112a3 30use SQL::Translator::Utils qw(debug header_comment parse_dbms_version batch_alter_table_statements normalize_quote_options);
2230ed2a 31use SQL::Translator::Generator::DDL::PostgreSQL;
f8f0253c 32use Data::Dumper;
33
9c9c169c 34use constant MAX_ID_LENGTH => 62;
35
f82112a3 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}
2230ed2a 49
b14cf6f4 50my ( %translate );
bfb5a568 51
52BEGIN {
53
54 %translate = (
d529894e 55 #
56 # MySQL types
57 #
90089d63 58 double => 'double precision',
4328d7bd 59 decimal => 'numeric',
d529894e 60 int => 'integer',
61 mediumint => 'integer',
d529894e 62 tinyint => 'smallint',
c8c17a58 63 char => 'character',
da8e499e 64 varchar => 'character varying',
d529894e 65 longtext => 'text',
66 mediumtext => 'text',
d529894e 67 tinytext => 'text',
68 tinyblob => 'bytea',
69 blob => 'bytea',
70 mediumblob => 'bytea',
71 longblob => 'bytea',
da8e499e 72 enum => 'character varying',
73 set => 'character varying',
d529894e 74 datetime => 'timestamp',
d529894e 75 year => 'date',
76
77 #
78 # Oracle types
79 #
96844cae 80 number => 'integer',
da8e499e 81 varchar2 => 'character varying',
96844cae 82 long => 'text',
ada2826e 83 clob => 'text',
96844cae 84
85 #
86 # Sybase types
87 #
96844cae 88 comment => 'text',
ebad0597 89
90 #
91 # MS Access types
92 #
93 memo => 'text',
d529894e 94);
bfb5a568 95}
96844cae 96my %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
109where 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
118and 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
da8e499e 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
96844cae 137=cut
f8f0253c 138
139sub produce {
e189562c 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;
7b4b17aa 147 my $postgres_version = parse_dbms_version(
148 $pargs->{postgres_version}, 'perl'
149 );
96844cae 150
f82112a3 151 my $generator = _generator({ quote_identifiers => $translator->quote_identifiers });
ea93df61 152
bf75adec 153 my @output;
154 push @output, header_comment unless ($no_comments);
96844cae 155
08d91aad 156 my (@table_defs, @fks);
79f55d7e 157 my %type_defs;
0c43e0a1 158 for my $table ( $schema->get_tables ) {
08d91aad 159
ea93df61 160 my ($table_def, $fks) = create_table($table, {
f82112a3 161 generator => $generator,
e189562c 162 no_comments => $no_comments,
163 postgres_version => $postgres_version,
164 add_drop_table => $add_drop_table,
79f55d7e 165 type_defs => \%type_defs,
e189562c 166 });
167
08d91aad 168 push @table_defs, $table_def;
169 push @fks, @$fks;
da8e499e 170 }
171
296c2701 172 for my $view ( $schema->get_views ) {
f82112a3 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 });
296c2701 179 }
180
c96cd4a8 181 for my $trigger ( $schema->get_triggers ) {
182 push @table_defs, create_trigger( $trigger, {
183 add_drop_trigger => $add_drop_table,
c9c8f3e1 184 generator => $generator,
c96cd4a8 185 no_comments => $no_comments,
186 });
187 }
188
79f55d7e 189 push @output, map { "$_;\n\n" } values %type_defs;
bf75adec 190 push @output, map { "$_;\n\n" } @table_defs;
08d91aad 191 if ( @fks ) {
bf75adec 192 push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
193 push @output, map { "$_;\n\n" } @fks;
08d91aad 194 }
021dbce8 195
da8e499e 196 if ( $WARN ) {
197 if ( %truncated ) {
198 warn "Truncated " . keys( %truncated ) . " names:\n";
199 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
200 }
f8f0253c 201 }
202
bf75adec 203 return wantarray
204 ? @output
205 : join ('', @output);
f8f0253c 206}
207
b14cf6f4 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 }
96844cae 230
b14cf6f4 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;
96844cae 237
b14cf6f4 238 warn "The name '$name_orig' has been changed to ",
239 "'$name' to make it unique.\n" if $WARN;
96844cae 240
b14cf6f4 241 $scope->{ $name_orig }++;
242 }
96844cae 243
b14cf6f4 244 $scope->{ $name }++;
245 return $name;
246 }
96844cae 247}
248
e83ad71c 249sub is_geometry
250{
ea93df61 251 my $field = shift;
252 return 1 if $field->data_type eq 'geometry';
140a1dad 253}
254
255sub is_geography
256{
257 my $field = shift;
258 return 1 if $field->data_type eq 'geography';
e83ad71c 259}
260
ea93df61 261sub create_table
bfb5a568 262{
263 my ($table, $options) = @_;
264
f82112a3 265 my $generator = _generator($options);
bfb5a568 266 my $no_comments = $options->{no_comments} || 0;
267 my $add_drop_table = $options->{add_drop_table} || 0;
5342f5c1 268 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 269 my $type_defs = $options->{type_defs} || {};
bfb5a568 270
3406fd5b 271 my $table_name = $table->name or next;
f82112a3 272 my $table_name_qt = $generator->quote($table_name);
bfb5a568 273
d31a1336 274 my ( @comments, @field_defs, @index_defs, @constraint_defs, @fks );
bfb5a568 275
f82112a3 276 push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
bfb5a568 277
31bed2c0 278 if ( !$no_comments and my $comments = $table->comments ) {
279 $comments =~ s/^/-- /mg;
280 push @comments, "-- Comments:\n$comments\n--\n";
bfb5a568 281 }
282
283 #
284 # Fields
285 #
bfb5a568 286 for my $field ( $table->get_fields ) {
f82112a3 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 });
bfb5a568 293 }
294
295 #
296 # Index Declarations
297 #
bfb5a568 298 for my $index ( $table->get_indices ) {
f82112a3 299 my ($idef, $constraints) = create_index($index, {
300 generator => $generator,
301 });
7ed7402c 302 $idef and push @index_defs, $idef;
bfb5a568 303 push @constraint_defs, @$constraints;
304 }
305
306 #
307 # Table constraints
308 #
bfb5a568 309 for my $c ( $table->get_constraints ) {
f82112a3 310 my ($cdefs, $fks) = create_constraint($c, {
311 generator => $generator,
312 });
bfb5a568 313 push @constraint_defs, @$cdefs;
314 push @fks, @$fks;
315 }
316
3e98f7d9 317
da936d63 318 my $create_statement = join("\n", @comments);
5342f5c1 319 if ($add_drop_table) {
7b4b17aa 320 if ($postgres_version >= 8.002) {
f82112a3 321 $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
5342f5c1 322 } else {
f82112a3 323 $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
5342f5c1 324 }
325 }
da936d63 326 my $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
f82112a3 327 $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n" .
bfb5a568 328 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
3406fd5b 329 "\n)"
bfb5a568 330 ;
3406fd5b 331 $create_statement .= @index_defs ? ';' : q{};
332 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
333 . join(";\n", @index_defs);
bfb5a568 334
ea93df61 335 #
336 # Geometry
337 #
338 if(grep { is_geometry($_) } $table->get_fields){
e83ad71c 339 $create_statement .= ";";
140a1dad 340 my @geometry_columns;
341 foreach my $col ($table->get_fields) { push(@geometry_columns,$col) if is_geometry($col); }
ea93df61 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 }
e83ad71c 345
08d91aad 346 return $create_statement, \@fks;
bfb5a568 347}
348
296c2701 349sub create_view {
350 my ($view, $options) = @_;
f82112a3 351 my $generator = _generator($options);
0e758018 352 my $postgres_version = $options->{postgres_version} || 0;
a25ac5d2 353 my $add_drop_view = $options->{add_drop_view};
296c2701 354
355 my $view_name = $view->name;
356 debug("PKG: Looking at view '${view_name}'\n");
357
358 my $create = '';
2230ed2a 359 $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
296c2701 360 unless $options->{no_comments};
0e758018 361 if ($add_drop_view) {
362 if ($postgres_version >= 8.002) {
2230ed2a 363 $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
0e758018 364 } else {
2230ed2a 365 $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
0e758018 366 }
367 }
296c2701 368 $create .= 'CREATE';
296c2701 369
370 my $extra = $view->extra;
371 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
2230ed2a 372 $create .= " VIEW " . $generator->quote($view_name);
296c2701 373
374 if ( my @fields = $view->fields ) {
2230ed2a 375 my $field_list = join ', ', map { $generator->quote($_) } @fields;
296c2701 376 $create .= " ( ${field_list} )";
377 }
378
379 if ( my $sql = $view->sql ) {
f59b2c0e 380 $create .= " AS\n ${sql}\n";
296c2701 381 }
382
383 if ( $extra->{check_option} ) {
384 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
385 }
386
296c2701 387 return $create;
388}
389
ea93df61 390{
bfb5a568 391
392 my %field_name_scope;
393
394 sub create_field
395 {
396 my ($field, $options) = @_;
397
f82112a3 398 my $generator = _generator($options);
bfb5a568 399 my $table_name = $field->table->name;
400 my $constraint_defs = $options->{constraint_defs} || [];
5342f5c1 401 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 402 my $type_defs = $options->{type_defs} || {};
bfb5a568 403
404 $field_name_scope{$table_name} ||= {};
912e67a1 405 my $field_name = $field->name;
31bed2c0 406 my $field_comments = '';
407 if (my $comments = $field->comments) {
408 $comments =~ s/(?<!\A)^/ -- /mg;
409 $field_comments = "-- $comments\n ";
410 }
bfb5a568 411
2230ed2a 412 my $field_def = $field_comments . $generator->quote($field_name);
bfb5a568 413
414 #
415 # Datatype
416 #
bfb5a568 417 my $data_type = lc $field->data_type;
418 my %extra = $field->extra;
419 my $list = $extra{'list'} || [];
1868ddbe 420 my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
bfb5a568 421
d31a1336 422 if ($postgres_version >= 8.003 && $data_type eq 'enum') {
79f55d7e 423 my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
5342f5c1 424 $field_def .= ' '. $type_name;
d25db732 425 my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
79f55d7e 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 }
5342f5c1 432 } else {
433 $field_def .= ' '. convert_datatype($field);
434 }
bfb5a568 435
436 #
ea93df61 437 # Default value
bfb5a568 438 #
1868ddbe 439 __PACKAGE__->_apply_default_value(
06baeb21 440 $field,
441 \$field_def,
442 [
443 'NULL' => \'NULL',
444 'now()' => 'now()',
445 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
446 ],
447 );
bfb5a568 448
449 #
450 # Not null constraint
451 #
452 $field_def .= ' NOT NULL' unless $field->is_nullable;
453
ea93df61 454 #
455 # Geometry constraints
456 #
457 if(is_geometry($field)){
458 foreach ( create_geometry_constraints($field) ) {
f82112a3 459 my ($cdefs, $fks) = create_constraint($_, {
460 generator => $generator,
461 });
ea93df61 462 push @$constraint_defs, @$cdefs;
463 push @$fks, @$fks;
464 }
e83ad71c 465 }
ea93df61 466
bfb5a568 467 return $field_def;
468 }
469}
470
e83ad71c 471sub create_geometry_constraints{
ea93df61 472 my $field = shift;
473
474 my @constraints;
475 push @constraints, SQL::Translator::Schema::Constraint->new(
476 name => "enforce_dims_".$field->name,
aa068030 477 expression => "(ST_NDims($field) = ".$field->extra->{dimensions}.")",
ea93df61 478 table => $field->table,
479 type => CHECK_C,
480 );
481
482 push @constraints, SQL::Translator::Schema::Constraint->new(
483 name => "enforce_srid_".$field->name,
aa068030 484 expression => "(ST_SRID($field) = ".$field->extra->{srid}.")",
ea93df61 485 table => $field->table,
486 type => CHECK_C,
487 );
488 push @constraints, SQL::Translator::Schema::Constraint->new(
489 name => "enforce_geotype_".$field->name,
aa068030 490 expression => "(GeometryType($field) = '".$field->extra->{geometry_type}."'::text OR $field IS NULL)",
ea93df61 491 table => $field->table,
492 type => CHECK_C,
493 );
494
495 return @constraints;
e83ad71c 496}
497
892573f2 498{
b14cf6f4 499 my %index_name;
500 sub create_index
501 {
502 my ($index, $options) = @_;
bfb5a568 503
b14cf6f4 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 }
5b36314d 532 }
5b36314d 533
b14cf6f4 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 }
bfb5a568 551
b14cf6f4 552 return $index_def, \@constraint_defs;
553 }
892573f2 554}
bfb5a568 555
892573f2 556sub create_constraint
557{
558 my ($c, $options) = @_;
bfb5a568 559
f82112a3 560 my $generator = _generator($options);
892573f2 561 my $table_name = $c->table->name;
562 my (@constraint_defs, @fks);
bfb5a568 563
892573f2 564 my $name = $c->name || '';
bfb5a568 565
5f31ed66 566 my @fields = grep { defined } $c->fields;
892573f2 567
5f31ed66 568 my @rfields = grep { defined } $c->reference_fields;
892573f2 569
570 next if !@fields && $c->type ne CHECK_C;
2230ed2a 571 my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
572 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
892573f2 573 if ( $c->type eq PRIMARY_KEY ) {
5f31ed66 574 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 575 }
576 elsif ( $c->type eq UNIQUE ) {
5f31ed66 577 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 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 ) {
2230ed2a 584 my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
585 . "\n REFERENCES " . $generator->quote($c->reference_table);
bfb5a568 586
892573f2 587 if ( @rfields ) {
2230ed2a 588 $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
892573f2 589 }
bfb5a568 590
892573f2 591 if ( $c->match_type ) {
ea93df61 592 $def .= ' MATCH ' .
892573f2 593 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
594 }
5342f5c1 595
892573f2 596 if ( $c->on_delete ) {
5863ad87 597 $def .= ' ON DELETE '. $c->on_delete;
892573f2 598 }
bfb5a568 599
892573f2 600 if ( $c->on_update ) {
5863ad87 601 $def .= ' ON UPDATE '. $c->on_update;
892573f2 602 }
124b192c 603
892573f2 604 if ( $c->deferrable ) {
605 $def .= ' DEFERRABLE';
124b192c 606 }
607
892573f2 608 push @fks, "$def";
124b192c 609 }
bfb5a568 610
892573f2 611 return \@constraint_defs, \@fks;
612}
613
c96cd4a8 614sub create_trigger {
615 my ($trigger,$options) = @_;
c9c8f3e1 616 my $generator = _generator($options);
c96cd4a8 617
618 my @statements;
619
c9c8f3e1 620 push @statements, sprintf( 'DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name) )
c96cd4a8 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',
c9c8f3e1 628 $generator->quote($trigger->name),
c96cd4a8 629 $trigger->perform_action_when,
630 join( ' OR ', @{ $trigger->database_events } ),
c9c8f3e1 631 $generator->quote($trigger->on_table),
c96cd4a8 632 $scope,
633 $trigger->action,
634 );
635
636 return @statements;
637}
638
bfb5a568 639sub convert_datatype
640{
641 my ($field) = @_;
642
643 my @size = $field->size;
644 my $data_type = lc $field->data_type;
aacb3187 645 my $array = $data_type =~ s/\[\]$//;
bfb5a568 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' );
ea93df61 651# push @$constraint_defs,
2230ed2a 652# 'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
bfb5a568 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 {
ada2826e 669 $data_type = defined $translate{ lc $data_type } ?
670 $translate{ lc $data_type } :
bfb5a568 671 $data_type;
672 }
673
ad258776 674 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
bfb5a568 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 }
52bc2e13 696
697 my $type_with_size = join('|',
e189562c 698 'bit', 'varbit', 'character', 'bit varying', 'character varying',
90089d63 699 'time', 'timestamp', 'interval', 'numeric', 'float'
52bc2e13 700 );
701
702 if ( $data_type !~ /$type_with_size/ ) {
ea93df61 703 @size = ();
e56dabb7 704 }
bfb5a568 705
ad258776 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 ) {
e189562c 710 $data_type .= '(' . join( ',', @size ) . ')';
08d91aad 711 }
aacb3187 712 if($array)
713 {
714 $data_type .= '[]';
715 }
bfb5a568 716
140a1dad 717 #
718 # Geography
719 #
720 if($data_type eq 'geography'){
aa068030 721 $data_type .= '('.$field->extra->{geography_type}.','. $field->extra->{srid} .')'
140a1dad 722 }
723
bfb5a568 724 return $data_type;
725}
726
727
728sub alter_field
729{
f7410082 730 my ($from_field, $to_field, $options) = @_;
bfb5a568 731
ea93df61 732 die "Can't alter field in another table"
bfb5a568 733 if($from_field->table->name ne $to_field->table->name);
734
f7410082 735 my $generator = _generator($options);
bfb5a568 736 my @out;
ea93df61 737
e83ad71c 738 # drop geometry column and constraints
c50d1a0a 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',
f7410082 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);
c50d1a0a 755
3406fd5b 756 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
f7410082 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);
bfb5a568 763
90726ffd 764 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
f7410082 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);
90726ffd 771
772
bfb5a568 773 my $from_dt = convert_datatype($from_field);
774 my $to_dt = convert_datatype($to_field);
3406fd5b 775 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
f7410082 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);
bfb5a568 783
3406fd5b 784 my $old_default = $from_field->default_value;
785 my $new_default = $to_field->default_value;
90726ffd 786 my $default_value = $to_field->default_value;
ea93df61 787
90726ffd 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 ) {
1868ddbe 793 $default_value = __PACKAGE__->_quote_string($default_value);
90726ffd 794 }
ea93df61 795
3406fd5b 796 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
f7410082 797 map($generator->quote($_),
798 $to_field->table->name,
799 $to_field->name,
800 ),
801 $default_value,
802 )
3406fd5b 803 if ( defined $new_default &&
804 (!defined $old_default || $old_default ne $new_default) );
bfb5a568 805
ea93df61 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',
f7410082 810 map($generator->quote($_),
811 $to_field->table->name,
812 $to_field->name,
813 ),
814 )
90726ffd 815 if ( !defined $new_default && defined $old_default );
ea93df61 816
10d7ce8b 817 # add geometry column and constraints
ea93df61 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
c50d1a0a 821 return wantarray ? @out : join(";\n", @out);
bfb5a568 822}
823
3406fd5b 824sub rename_field { alter_field(@_) }
825
bfb5a568 826sub add_field
827{
f7410082 828 my ($new_field,$options) = @_;
bfb5a568 829
3406fd5b 830 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
f7410082 831 _generator($options)->quote($new_field->table->name),
832 create_field($new_field, $options));
e83ad71c 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);
bfb5a568 835 return $out;
836
837}
838
839sub drop_field
840{
c50d1a0a 841 my ($old_field, $options) = @_;
842
f82112a3 843 my $generator = _generator($options);
bfb5a568 844
3406fd5b 845 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
2230ed2a 846 $generator->quote($old_field->table->name),
847 $generator->quote($old_field->name));
c50d1a0a 848 $out .= "\n".drop_geometry_column($old_field) if is_geometry($old_field);
ea93df61 849 return $out;
bfb5a568 850}
851
e83ad71c 852sub add_geometry_column{
ea93df61 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,
aa068030 860 $field->extra->{dimensions},
861 $field->extra->{srid},
862 $field->extra->{geometry_type});
e83ad71c 863 return $out;
864}
865
866sub drop_geometry_column
867{
ea93df61 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);
e83ad71c 874 return $out;
875}
876
877sub add_geometry_constraints{
ea93df61 878 my $field = shift;
879
880 my @constraints = create_geometry_constraints($field);
881
882 my $out = join("\n", map { alter_create_constraint($_); } @constraints);
e83ad71c 883
ea93df61 884 return $out;
e83ad71c 885}
886
887sub drop_geometry_constraints{
ea93df61 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;
e83ad71c 895}
896
3406fd5b 897sub alter_table {
898 my ($to_table, $options) = @_;
f82112a3 899 my $generator = _generator($options);
3406fd5b 900 my $out = sprintf('ALTER TABLE %s %s',
2230ed2a 901 $generator->quote($to_table->name),
3406fd5b 902 $options->{alter_table_action});
e83ad71c 903 $out .= "\n".$options->{geometry_changes} if $options->{geometry_changes};
3406fd5b 904 return $out;
905}
906
907sub rename_table {
908 my ($old_table, $new_table, $options) = @_;
f82112a3 909 my $generator = _generator($options);
2230ed2a 910 $options->{alter_table_action} = "RENAME TO " . $generator->quote($new_table);
e83ad71c 911
ea93df61 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
e83ad71c 916 $options->{geometry_changes} = join ("\n",@geometry_changes) if scalar(@geometry_changes);
ea93df61 917
3406fd5b 918 return alter_table($old_table, $options);
919}
920
921sub alter_create_index {
922 my ($index, $options) = @_;
f82112a3 923 my $generator = _generator($options);
3406fd5b 924 my ($idef, $constraints) = create_index($index, {
f82112a3 925 generator => $generator,
3406fd5b 926 });
927 return $index->type eq NORMAL ? $idef
928 : sprintf('ALTER TABLE %s ADD %s',
2230ed2a 929 $generator->quote($index->table->name),
3406fd5b 930 join(q{}, @$constraints)
931 );
932}
933
934sub alter_drop_index {
935 my ($index, $options) = @_;
f7410082 936 return 'DROP INDEX '. _generator($options)->quote($index->name);
3406fd5b 937}
938
939sub alter_drop_constraint {
940 my ($c, $options) = @_;
f82112a3 941 my $generator = _generator($options);
c50d1a0a 942
bc9b1c11 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 ) {
f7410082 948 # Already has a name, just use it
949 $c_name = $c->name;
bc9b1c11 950 } elsif ( $c->type eq FOREIGN_KEY ) {
951 # Doesn't have a name, and is foreign key, append '_fkey'
f7410082 952 $c_name = $c->table->name . '_' . ($c->fields)[0] . '_fkey';
bc9b1c11 953 } elsif ( $c->type eq PRIMARY_KEY ) {
954 # Doesn't have a name, and is primary key, append '_pkey'
f7410082 955 $c_name = $c->table->name . '_pkey';
bc9b1c11 956 }
957
c50d1a0a 958 return sprintf(
959 'ALTER TABLE %s DROP CONSTRAINT %s',
f7410082 960 map { $generator->quote($_) } $c->table->name, $c_name,
c50d1a0a 961 );
3406fd5b 962}
963
964sub alter_create_constraint {
965 my ($index, $options) = @_;
f82112a3 966 my $generator = _generator($options);
90726ffd 967 my ($defs, $fks) = create_constraint(@_);
ea93df61 968
90726ffd 969 # return if there are no constraint definitions so we don't run
970 # into output like this:
971 # ALTER TABLE users ADD ;
ea93df61 972
90726ffd 973 return unless(@{$defs} || @{$fks});
974 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
2230ed2a 975 : join( ' ', 'ALTER TABLE', $generator->quote($index->table->name),
90726ffd 976 'ADD', join(q{}, @{$defs}, @{$fks})
3406fd5b 977 );
978}
979
980sub drop_table {
981 my ($table, $options) = @_;
f82112a3 982 my $generator = _generator($options);
2230ed2a 983 my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
ea93df61 984
e83ad71c 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;
3406fd5b 989}
990
0c610cc8 991sub batch_alter_table {
992 my ( $table, $diff_hash, $options ) = @_;
0c610cc8 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 ) {
86609eaa 996 return batch_alter_table_statements($diff_hash, $options);
0c610cc8 997 }
998
999 # first we need to perform drops which are on old table
86609eaa 1000 my @sql = batch_alter_table_statements($diff_hash, $options, qw(
1001 alter_drop_constraint
0c610cc8 1002 alter_drop_index
86609eaa 1003 drop_field
1004 ));
0c610cc8 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
86609eaa 1018 push @sql, batch_alter_table_statements($diff_hash, $options, qw(
1019 add_field
0c610cc8 1020 alter_field
1021 rename_field
1022 alter_create_index
1023 alter_create_constraint
86609eaa 1024 alter_table
1025 ));
0c610cc8 1026
1027 return @sql;
1028}
1029
f8f0253c 10301;
f8f0253c 1031
96844cae 1032# -------------------------------------------------------------------
1033# Life is full of misery, loneliness, and suffering --
1034# and it's all over much too soon.
1035# Woody Allen
1036# -------------------------------------------------------------------
f8f0253c 1037
96844cae 1038=pod
f8f0253c 1039
20770e44 1040=head1 SEE ALSO
1041
1042SQL::Translator, SQL::Translator::Producer::Oracle.
1043
f8f0253c 1044=head1 AUTHOR
1045
f997b9ab 1046Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
f8f0253c 1047
1048=cut