Bumping version to 1.61
[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 );
752a0ffc 25our $VERSION = '1.61';
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
0b4b7709 249sub is_geometry {
250 my $field = shift;
251 return 1 if $field->data_type eq 'geometry';
140a1dad 252}
253
0b4b7709 254sub is_geography {
140a1dad 255 my $field = shift;
256 return 1 if $field->data_type eq 'geography';
e83ad71c 257}
258
ea93df61 259sub create_table
bfb5a568 260{
261 my ($table, $options) = @_;
262
f82112a3 263 my $generator = _generator($options);
bfb5a568 264 my $no_comments = $options->{no_comments} || 0;
265 my $add_drop_table = $options->{add_drop_table} || 0;
5342f5c1 266 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 267 my $type_defs = $options->{type_defs} || {};
bfb5a568 268
3406fd5b 269 my $table_name = $table->name or next;
f82112a3 270 my $table_name_qt = $generator->quote($table_name);
bfb5a568 271
d31a1336 272 my ( @comments, @field_defs, @index_defs, @constraint_defs, @fks );
bfb5a568 273
f82112a3 274 push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
bfb5a568 275
31bed2c0 276 if ( !$no_comments and my $comments = $table->comments ) {
277 $comments =~ s/^/-- /mg;
278 push @comments, "-- Comments:\n$comments\n--\n";
bfb5a568 279 }
280
281 #
282 # Fields
283 #
bfb5a568 284 for my $field ( $table->get_fields ) {
f82112a3 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 });
bfb5a568 291 }
292
293 #
294 # Index Declarations
295 #
bfb5a568 296 for my $index ( $table->get_indices ) {
f82112a3 297 my ($idef, $constraints) = create_index($index, {
298 generator => $generator,
299 });
7ed7402c 300 $idef and push @index_defs, $idef;
bfb5a568 301 push @constraint_defs, @$constraints;
302 }
303
304 #
305 # Table constraints
306 #
bfb5a568 307 for my $c ( $table->get_constraints ) {
f82112a3 308 my ($cdefs, $fks) = create_constraint($c, {
309 generator => $generator,
310 });
bfb5a568 311 push @constraint_defs, @$cdefs;
312 push @fks, @$fks;
313 }
314
3e98f7d9 315
da936d63 316 my $create_statement = join("\n", @comments);
5342f5c1 317 if ($add_drop_table) {
7b4b17aa 318 if ($postgres_version >= 8.002) {
f82112a3 319 $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
5342f5c1 320 } else {
f82112a3 321 $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
5342f5c1 322 }
323 }
da936d63 324 my $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
f82112a3 325 $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n" .
bfb5a568 326 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
3406fd5b 327 "\n)"
bfb5a568 328 ;
3406fd5b 329 $create_statement .= @index_defs ? ';' : q{};
330 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
331 . join(";\n", @index_defs);
bfb5a568 332
0b4b7709 333 #
334 # Geometry
335 #
54b8ff8c 336 if (my @geometry_columns = grep { is_geometry($_) } $table->get_fields) {
3963e31d 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);
0b4b7709 339 }
e83ad71c 340
08d91aad 341 return $create_statement, \@fks;
bfb5a568 342}
343
296c2701 344sub create_view {
345 my ($view, $options) = @_;
f82112a3 346 my $generator = _generator($options);
0e758018 347 my $postgres_version = $options->{postgres_version} || 0;
a25ac5d2 348 my $add_drop_view = $options->{add_drop_view};
296c2701 349
350 my $view_name = $view->name;
351 debug("PKG: Looking at view '${view_name}'\n");
352
353 my $create = '';
2230ed2a 354 $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
296c2701 355 unless $options->{no_comments};
0e758018 356 if ($add_drop_view) {
357 if ($postgres_version >= 8.002) {
2230ed2a 358 $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
0e758018 359 } else {
2230ed2a 360 $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
0e758018 361 }
362 }
296c2701 363 $create .= 'CREATE';
296c2701 364
365 my $extra = $view->extra;
366 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
2230ed2a 367 $create .= " VIEW " . $generator->quote($view_name);
296c2701 368
369 if ( my @fields = $view->fields ) {
2230ed2a 370 my $field_list = join ', ', map { $generator->quote($_) } @fields;
296c2701 371 $create .= " ( ${field_list} )";
372 }
373
374 if ( my $sql = $view->sql ) {
f59b2c0e 375 $create .= " AS\n ${sql}\n";
296c2701 376 }
377
378 if ( $extra->{check_option} ) {
379 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
380 }
381
296c2701 382 return $create;
383}
384
ea93df61 385{
bfb5a568 386
387 my %field_name_scope;
388
389 sub create_field
390 {
391 my ($field, $options) = @_;
392
f82112a3 393 my $generator = _generator($options);
bfb5a568 394 my $table_name = $field->table->name;
395 my $constraint_defs = $options->{constraint_defs} || [];
5342f5c1 396 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 397 my $type_defs = $options->{type_defs} || {};
bfb5a568 398
399 $field_name_scope{$table_name} ||= {};
912e67a1 400 my $field_name = $field->name;
31bed2c0 401 my $field_comments = '';
402 if (my $comments = $field->comments) {
403 $comments =~ s/(?<!\A)^/ -- /mg;
404 $field_comments = "-- $comments\n ";
405 }
bfb5a568 406
2230ed2a 407 my $field_def = $field_comments . $generator->quote($field_name);
bfb5a568 408
409 #
410 # Datatype
411 #
bfb5a568 412 my $data_type = lc $field->data_type;
413 my %extra = $field->extra;
414 my $list = $extra{'list'} || [];
1868ddbe 415 my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
bfb5a568 416
d31a1336 417 if ($postgres_version >= 8.003 && $data_type eq 'enum') {
79f55d7e 418 my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
5342f5c1 419 $field_def .= ' '. $type_name;
d25db732 420 my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
79f55d7e 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 }
5342f5c1 427 } else {
428 $field_def .= ' '. convert_datatype($field);
429 }
bfb5a568 430
431 #
ea93df61 432 # Default value
bfb5a568 433 #
1868ddbe 434 __PACKAGE__->_apply_default_value(
06baeb21 435 $field,
436 \$field_def,
437 [
438 'NULL' => \'NULL',
439 'now()' => 'now()',
440 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
441 ],
442 );
bfb5a568 443
444 #
445 # Not null constraint
446 #
447 $field_def .= ' NOT NULL' unless $field->is_nullable;
448
0b4b7709 449 #
450 # Geometry constraints
451 #
452 if (is_geometry($field)) {
3963e31d 453 foreach ( create_geometry_constraints($field, $options) ) {
b4414534 454 my ($cdefs, $fks) = create_constraint($_, $options);
0b4b7709 455 push @$constraint_defs, @$cdefs;
456 push @$fks, @$fks;
457 }
e83ad71c 458 }
ea93df61 459
bfb5a568 460 return $field_def;
461 }
462}
463
0b4b7709 464sub create_geometry_constraints {
3963e31d 465 my ($field, $options) = @_;
0b4b7709 466
3963e31d 467 my $fname = _generator($options)->quote($field);
0b4b7709 468 my @constraints;
469 push @constraints, SQL::Translator::Schema::Constraint->new(
470 name => "enforce_dims_".$field->name,
3963e31d 471 expression => "(ST_NDims($fname) = ".$field->extra->{dimensions}.")",
0b4b7709 472 table => $field->table,
473 type => CHECK_C,
474 );
475
476 push @constraints, SQL::Translator::Schema::Constraint->new(
477 name => "enforce_srid_".$field->name,
3963e31d 478 expression => "(ST_SRID($fname) = ".$field->extra->{srid}.")",
0b4b7709 479 table => $field->table,
480 type => CHECK_C,
481 );
482 push @constraints, SQL::Translator::Schema::Constraint->new(
483 name => "enforce_geotype_".$field->name,
3963e31d 484 expression => "(GeometryType($fname) = ". __PACKAGE__->_quote_string($field->extra->{geometry_type}) ."::text OR $fname IS NULL)",
0b4b7709 485 table => $field->table,
486 type => CHECK_C,
487 );
488
489 return @constraints;
e83ad71c 490}
491
892573f2 492{
b14cf6f4 493 my %index_name;
494 sub create_index
495 {
496 my ($index, $options) = @_;
bfb5a568 497
b14cf6f4 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 }
5b36314d 526 }
5b36314d 527
b14cf6f4 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 }
bfb5a568 545
b14cf6f4 546 return $index_def, \@constraint_defs;
547 }
892573f2 548}
bfb5a568 549
892573f2 550sub create_constraint
551{
552 my ($c, $options) = @_;
bfb5a568 553
f82112a3 554 my $generator = _generator($options);
892573f2 555 my $table_name = $c->table->name;
556 my (@constraint_defs, @fks);
bfb5a568 557
892573f2 558 my $name = $c->name || '';
bfb5a568 559
5f31ed66 560 my @fields = grep { defined } $c->fields;
892573f2 561
5f31ed66 562 my @rfields = grep { defined } $c->reference_fields;
892573f2 563
564 next if !@fields && $c->type ne CHECK_C;
2230ed2a 565 my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
566 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
892573f2 567 if ( $c->type eq PRIMARY_KEY ) {
5f31ed66 568 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 569 }
570 elsif ( $c->type eq UNIQUE ) {
5f31ed66 571 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 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 ) {
2230ed2a 578 my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
579 . "\n REFERENCES " . $generator->quote($c->reference_table);
bfb5a568 580
892573f2 581 if ( @rfields ) {
2230ed2a 582 $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
892573f2 583 }
bfb5a568 584
892573f2 585 if ( $c->match_type ) {
ea93df61 586 $def .= ' MATCH ' .
892573f2 587 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
588 }
5342f5c1 589
892573f2 590 if ( $c->on_delete ) {
5863ad87 591 $def .= ' ON DELETE '. $c->on_delete;
892573f2 592 }
bfb5a568 593
892573f2 594 if ( $c->on_update ) {
5863ad87 595 $def .= ' ON UPDATE '. $c->on_update;
892573f2 596 }
124b192c 597
892573f2 598 if ( $c->deferrable ) {
599 $def .= ' DEFERRABLE';
124b192c 600 }
601
892573f2 602 push @fks, "$def";
124b192c 603 }
bfb5a568 604
892573f2 605 return \@constraint_defs, \@fks;
606}
607
c96cd4a8 608sub create_trigger {
609 my ($trigger,$options) = @_;
c9c8f3e1 610 my $generator = _generator($options);
c96cd4a8 611
612 my @statements;
613
c9c8f3e1 614 push @statements, sprintf( 'DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name) )
c96cd4a8 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',
c9c8f3e1 622 $generator->quote($trigger->name),
c96cd4a8 623 $trigger->perform_action_when,
624 join( ' OR ', @{ $trigger->database_events } ),
c9c8f3e1 625 $generator->quote($trigger->on_table),
c96cd4a8 626 $scope,
627 $trigger->action,
628 );
629
630 return @statements;
631}
632
bfb5a568 633sub convert_datatype
634{
635 my ($field) = @_;
636
637 my @size = $field->size;
638 my $data_type = lc $field->data_type;
aacb3187 639 my $array = $data_type =~ s/\[\]$//;
bfb5a568 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' );
ea93df61 645# push @$constraint_defs,
2230ed2a 646# 'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
bfb5a568 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 ) {
9d430e09 654 if ( (defined $size[0] && $size[0] > 11) or $data_type eq 'bigint' ) {
bfb5a568 655 $data_type = 'bigserial';
656 }
657 else {
658 $data_type = 'serial';
659 }
660 undef @size;
661 }
662 else {
ada2826e 663 $data_type = defined $translate{ lc $data_type } ?
664 $translate{ lc $data_type } :
bfb5a568 665 $data_type;
666 }
667
ad258776 668 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
bfb5a568 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 }
52bc2e13 690
691 my $type_with_size = join('|',
e189562c 692 'bit', 'varbit', 'character', 'bit varying', 'character varying',
90089d63 693 'time', 'timestamp', 'interval', 'numeric', 'float'
52bc2e13 694 );
695
696 if ( $data_type !~ /$type_with_size/ ) {
ea93df61 697 @size = ();
e56dabb7 698 }
bfb5a568 699
ad258776 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 ) {
e189562c 704 $data_type .= '(' . join( ',', @size ) . ')';
08d91aad 705 }
aacb3187 706 if($array)
707 {
708 $data_type .= '[]';
709 }
bfb5a568 710
140a1dad 711 #
712 # Geography
713 #
714 if($data_type eq 'geography'){
aa068030 715 $data_type .= '('.$field->extra->{geography_type}.','. $field->extra->{srid} .')'
140a1dad 716 }
717
bfb5a568 718 return $data_type;
719}
720
721
722sub alter_field
723{
f7410082 724 my ($from_field, $to_field, $options) = @_;
bfb5a568 725
ea93df61 726 die "Can't alter field in another table"
bfb5a568 727 if($from_field->table->name ne $to_field->table->name);
728
f7410082 729 my $generator = _generator($options);
bfb5a568 730 my @out;
ea93df61 731
e83ad71c 732 # drop geometry column and constraints
54b8ff8c 733 push @out,
3963e31d 734 drop_geometry_column($from_field, $options),
735 drop_geometry_constraints($from_field, $options),
54b8ff8c 736 if is_geometry($from_field);
c50d1a0a 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',
f7410082 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);
c50d1a0a 751
3406fd5b 752 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
f7410082 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);
bfb5a568 759
90726ffd 760 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
f7410082 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);
90726ffd 767
768
bfb5a568 769 my $from_dt = convert_datatype($from_field);
770 my $to_dt = convert_datatype($to_field);
3406fd5b 771 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
f7410082 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);
bfb5a568 779
3406fd5b 780 my $old_default = $from_field->default_value;
781 my $new_default = $to_field->default_value;
90726ffd 782 my $default_value = $to_field->default_value;
ea93df61 783
90726ffd 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;
3f73e2f3 788 } elsif( defined $default_value && $to_dt =~ /^(character|text|timestamp|date)/xsmi ) {
1868ddbe 789 $default_value = __PACKAGE__->_quote_string($default_value);
90726ffd 790 }
ea93df61 791
3406fd5b 792 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
f7410082 793 map($generator->quote($_),
794 $to_field->table->name,
795 $to_field->name,
796 ),
797 $default_value,
798 )
3406fd5b 799 if ( defined $new_default &&
800 (!defined $old_default || $old_default ne $new_default) );
bfb5a568 801
ea93df61 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',
f7410082 806 map($generator->quote($_),
807 $to_field->table->name,
808 $to_field->name,
809 ),
810 )
90726ffd 811 if ( !defined $new_default && defined $old_default );
ea93df61 812
10d7ce8b 813 # add geometry column and constraints
54b8ff8c 814 push @out,
3963e31d 815 add_geometry_column($to_field, $options),
816 add_geometry_constraints($to_field, $options),
54b8ff8c 817 if is_geometry($to_field);
ea93df61 818
c50d1a0a 819 return wantarray ? @out : join(";\n", @out);
bfb5a568 820}
821
3406fd5b 822sub rename_field { alter_field(@_) }
823
bfb5a568 824sub add_field
825{
f7410082 826 my ($new_field,$options) = @_;
bfb5a568 827
3406fd5b 828 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
f7410082 829 _generator($options)->quote($new_field->table->name),
830 create_field($new_field, $options));
3963e31d 831 $out .= ";\n".add_geometry_column($new_field, $options)
832 . ";\n".add_geometry_constraints($new_field, $options)
54b8ff8c 833 if is_geometry($new_field);
bfb5a568 834 return $out;
835
836}
837
838sub drop_field
839{
c50d1a0a 840 my ($old_field, $options) = @_;
841
f82112a3 842 my $generator = _generator($options);
bfb5a568 843
3406fd5b 844 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
2230ed2a 845 $generator->quote($old_field->table->name),
846 $generator->quote($old_field->name));
3963e31d 847 $out .= ";\n".drop_geometry_column($old_field, $options)
54b8ff8c 848 if is_geometry($old_field);
ea93df61 849 return $out;
bfb5a568 850}
851
0b4b7709 852sub add_geometry_column {
54b8ff8c 853 my ($field, $options) = @_;
0b4b7709 854
54b8ff8c 855 return sprintf(
724347d1 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 ),
0b4b7709 866 );
e83ad71c 867}
868
0b4b7709 869sub drop_geometry_column {
724347d1 870 my ($field) = @_;
ea93df61 871
54b8ff8c 872 return sprintf(
724347d1 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 ),
0b4b7709 879 );
e83ad71c 880}
881
0b4b7709 882sub add_geometry_constraints {
54b8ff8c 883 my ($field, $options) = @_;
ea93df61 884
3963e31d 885 return join(";\n", map { alter_create_constraint($_, $options) }
886 create_geometry_constraints($field, $options));
e83ad71c 887}
888
0b4b7709 889sub drop_geometry_constraints {
54b8ff8c 890 my ($field, $options) = @_;
ea93df61 891
3963e31d 892 return join(";\n", map { alter_drop_constraint($_, $options) }
893 create_geometry_constraints($field, $options));
ea93df61 894
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});
6440741a 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
54b8ff8c 912 my @geometry_changes = map {
3963e31d 913 drop_geometry_column($_, $options),
914 add_geometry_column($_, { %{$options}, table => $new_table }),
54b8ff8c 915 } grep { is_geometry($_) } $old_table->get_fields;
ea93df61 916
6440741a 917 $options->{geometry_changes} = join (";\n",@geometry_changes) if @geometry_changes;
ea93df61 918
3406fd5b 919 return alter_table($old_table, $options);
920}
921
922sub alter_create_index {
923 my ($index, $options) = @_;
f82112a3 924 my $generator = _generator($options);
b4414534 925 my ($idef, $constraints) = create_index($index, $options);
3406fd5b 926 return $index->type eq NORMAL ? $idef
927 : sprintf('ALTER TABLE %s ADD %s',
2230ed2a 928 $generator->quote($index->table->name),
3406fd5b 929 join(q{}, @$constraints)
930 );
931}
932
933sub alter_drop_index {
934 my ($index, $options) = @_;
f7410082 935 return 'DROP INDEX '. _generator($options)->quote($index->name);
3406fd5b 936}
937
938sub alter_drop_constraint {
939 my ($c, $options) = @_;
f82112a3 940 my $generator = _generator($options);
c50d1a0a 941
bc9b1c11 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 ) {
f7410082 947 # Already has a name, just use it
948 $c_name = $c->name;
bc9b1c11 949 } elsif ( $c->type eq FOREIGN_KEY ) {
950 # Doesn't have a name, and is foreign key, append '_fkey'
f7410082 951 $c_name = $c->table->name . '_' . ($c->fields)[0] . '_fkey';
bc9b1c11 952 } elsif ( $c->type eq PRIMARY_KEY ) {
953 # Doesn't have a name, and is primary key, append '_pkey'
f7410082 954 $c_name = $c->table->name . '_pkey';
bc9b1c11 955 }
956
c50d1a0a 957 return sprintf(
958 'ALTER TABLE %s DROP CONSTRAINT %s',
f7410082 959 map { $generator->quote($_) } $c->table->name, $c_name,
c50d1a0a 960 );
3406fd5b 961}
962
963sub alter_create_constraint {
964 my ($index, $options) = @_;
f82112a3 965 my $generator = _generator($options);
90726ffd 966 my ($defs, $fks) = create_constraint(@_);
ea93df61 967
90726ffd 968 # return if there are no constraint definitions so we don't run
969 # into output like this:
970 # ALTER TABLE users ADD ;
ea93df61 971
90726ffd 972 return unless(@{$defs} || @{$fks});
973 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
2230ed2a 974 : join( ' ', 'ALTER TABLE', $generator->quote($index->table->name),
90726ffd 975 'ADD', join(q{}, @{$defs}, @{$fks})
3406fd5b 976 );
977}
978
979sub drop_table {
980 my ($table, $options) = @_;
f82112a3 981 my $generator = _generator($options);
2230ed2a 982 my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
ea93df61 983
e83ad71c 984 my @geometry_drops = map { drop_geometry_column($_); } grep { is_geometry($_) } $table->get_fields;
985
6440741a 986 $out .= join(";\n", '', @geometry_drops) if @geometry_drops;
e83ad71c 987 return $out;
3406fd5b 988}
989
0c610cc8 990sub batch_alter_table {
991 my ( $table, $diff_hash, $options ) = @_;
0c610cc8 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 ) {
86609eaa 995 return batch_alter_table_statements($diff_hash, $options);
0c610cc8 996 }
997
998 # first we need to perform drops which are on old table
86609eaa 999 my @sql = batch_alter_table_statements($diff_hash, $options, qw(
1000 alter_drop_constraint
0c610cc8 1001 alter_drop_index
86609eaa 1002 drop_field
1003 ));
0c610cc8 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
86609eaa 1017 push @sql, batch_alter_table_statements($diff_hash, $options, qw(
1018 add_field
0c610cc8 1019 alter_field
1020 rename_field
1021 alter_create_index
1022 alter_create_constraint
86609eaa 1023 alter_table
1024 ));
0c610cc8 1025
1026 return @sql;
1027}
1028
f8f0253c 10291;
f8f0253c 1030
96844cae 1031# -------------------------------------------------------------------
1032# Life is full of misery, loneliness, and suffering --
1033# and it's all over much too soon.
1034# Woody Allen
1035# -------------------------------------------------------------------
f8f0253c 1036
96844cae 1037=pod
f8f0253c 1038
20770e44 1039=head1 SEE ALSO
1040
1041SQL::Translator, SQL::Translator::Producer::Oracle.
1042
f8f0253c 1043=head1 AUTHOR
1044
f997b9ab 1045Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
f8f0253c 1046
1047=cut