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