Fix quoting of trigger name and table in Producer::PostgreSQL
[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
f82112a3 34{
35 my ($quoting_generator, $nonquoting_generator);
36 sub _generator {
37 my $options = shift;
38 return $options->{generator} if exists $options->{generator};
39
40 return normalize_quote_options($options)
41 ? $quoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new
42 : $nonquoting_generator ||= SQL::Translator::Generator::DDL::PostgreSQL->new(
43 quote_chars => [],
44 );
45 }
46}
2230ed2a 47
892573f2 48my ( %translate, %index_name );
bfb5a568 49my $max_id_length;
50
51BEGIN {
52
53 %translate = (
d529894e 54 #
55 # MySQL types
56 #
57 bigint => 'bigint',
90089d63 58 double => 'double precision',
4328d7bd 59 decimal => 'numeric',
d529894e 60 int => 'integer',
61 mediumint => 'integer',
62 smallint => 'smallint',
63 tinyint => 'smallint',
c8c17a58 64 char => 'character',
da8e499e 65 varchar => 'character varying',
d529894e 66 longtext => 'text',
67 mediumtext => 'text',
68 text => 'text',
69 tinytext => 'text',
70 tinyblob => 'bytea',
71 blob => 'bytea',
72 mediumblob => 'bytea',
73 longblob => 'bytea',
da8e499e 74 enum => 'character varying',
75 set => 'character varying',
d529894e 76 date => 'date',
77 datetime => 'timestamp',
e56dabb7 78 time => 'time',
d529894e 79 timestamp => 'timestamp',
80 year => 'date',
81
82 #
83 # Oracle types
84 #
96844cae 85 number => 'integer',
c8c17a58 86 char => 'character',
da8e499e 87 varchar2 => 'character varying',
96844cae 88 long => 'text',
89 CLOB => 'bytea',
90 date => 'date',
91
92 #
93 # Sybase types
94 #
95 int => 'integer',
96 money => 'money',
da8e499e 97 varchar => 'character varying',
96844cae 98 datetime => 'timestamp',
99 text => 'text',
96844cae 100 comment => 'text',
101 bit => 'bit',
102 tinyint => 'smallint',
d529894e 103);
104
bfb5a568 105 $max_id_length = 62;
106}
96844cae 107my %reserved = map { $_, 1 } qw[
ea93df61 108 ALL ANALYSE ANALYZE AND ANY AS ASC
96844cae 109 BETWEEN BINARY BOTH
110 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
ea93df61 111 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
96844cae 112 DEFAULT DEFERRABLE DESC DISTINCT DO
113 ELSE END EXCEPT
ea93df61 114 FALSE FOR FOREIGN FREEZE FROM FULL
115 GROUP HAVING
116 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
117 JOIN LEADING LEFT LIKE LIMIT
96844cae 118 NATURAL NEW NOT NOTNULL NULL
119 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
ea93df61 120 PRIMARY PUBLIC REFERENCES RIGHT
121 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
96844cae 122 UNION UNIQUE USER USING VERBOSE WHEN WHERE
123];
d529894e 124
bfb5a568 125# my $max_id_length = 62;
96844cae 126my %used_identifiers = ();
127my %global_names;
96844cae 128my %truncated;
129
130=pod
131
132=head1 PostgreSQL Create Table Syntax
133
134 CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
135 { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
136 | table_constraint } [, ... ]
137 )
138 [ INHERITS ( parent_table [, ... ] ) ]
139 [ WITH OIDS | WITHOUT OIDS ]
140
141where column_constraint is:
142
143 [ CONSTRAINT constraint_name ]
144 { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
145 CHECK (expression) |
146 REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
147 [ ON DELETE action ] [ ON UPDATE action ] }
148 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
149
150and table_constraint is:
151
152 [ CONSTRAINT constraint_name ]
153 { UNIQUE ( column_name [, ... ] ) |
154 PRIMARY KEY ( column_name [, ... ] ) |
155 CHECK ( expression ) |
156 FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
157 [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
158 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
159
da8e499e 160=head1 Create Index Syntax
161
162 CREATE [ UNIQUE ] INDEX index_name ON table
163 [ USING acc_method ] ( column [ ops_name ] [, ...] )
164 [ WHERE predicate ]
165 CREATE [ UNIQUE ] INDEX index_name ON table
166 [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
167 [ WHERE predicate ]
168
96844cae 169=cut
f8f0253c 170
171sub produce {
e189562c 172 my $translator = shift;
173 local $DEBUG = $translator->debug;
174 local $WARN = $translator->show_warnings;
175 my $no_comments = $translator->no_comments;
176 my $add_drop_table = $translator->add_drop_table;
177 my $schema = $translator->schema;
178 my $pargs = $translator->producer_args;
7b4b17aa 179 my $postgres_version = parse_dbms_version(
180 $pargs->{postgres_version}, 'perl'
181 );
96844cae 182
f82112a3 183 my $generator = _generator({ quote_identifiers => $translator->quote_identifiers });
ea93df61 184
bf75adec 185 my @output;
186 push @output, header_comment unless ($no_comments);
96844cae 187
08d91aad 188 my (@table_defs, @fks);
79f55d7e 189 my %type_defs;
0c43e0a1 190 for my $table ( $schema->get_tables ) {
08d91aad 191
ea93df61 192 my ($table_def, $fks) = create_table($table, {
f82112a3 193 generator => $generator,
e189562c 194 no_comments => $no_comments,
195 postgres_version => $postgres_version,
196 add_drop_table => $add_drop_table,
79f55d7e 197 type_defs => \%type_defs,
e189562c 198 });
199
08d91aad 200 push @table_defs, $table_def;
201 push @fks, @$fks;
da8e499e 202 }
203
296c2701 204 for my $view ( $schema->get_views ) {
f82112a3 205 push @table_defs, create_view($view, {
206 postgres_version => $postgres_version,
207 add_drop_view => $add_drop_table,
208 generator => $generator,
209 no_comments => $no_comments,
210 });
296c2701 211 }
212
c96cd4a8 213 for my $trigger ( $schema->get_triggers ) {
214 push @table_defs, create_trigger( $trigger, {
215 add_drop_trigger => $add_drop_table,
c9c8f3e1 216 generator => $generator,
c96cd4a8 217 no_comments => $no_comments,
218 });
219 }
220
79f55d7e 221 push @output, map { "$_;\n\n" } values %type_defs;
bf75adec 222 push @output, map { "$_;\n\n" } @table_defs;
08d91aad 223 if ( @fks ) {
bf75adec 224 push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
225 push @output, map { "$_;\n\n" } @fks;
08d91aad 226 }
021dbce8 227
da8e499e 228 if ( $WARN ) {
229 if ( %truncated ) {
230 warn "Truncated " . keys( %truncated ) . " names:\n";
231 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
232 }
f8f0253c 233 }
234
bf75adec 235 return wantarray
236 ? @output
237 : join ('', @output);
f8f0253c 238}
239
96844cae 240sub mk_name {
ea93df61 241 my $basename = shift || '';
242 my $type = shift || '';
243 my $scope = shift || '';
0c43e0a1 244 my $critical = shift || '';
96844cae 245 my $basename_orig = $basename;
bfb5a568 246# my $max_id_length = 62;
ea93df61 247 my $max_name = $type
248 ? $max_id_length - (length($type) + 1)
2ad4c2c8 249 : $max_id_length;
ea93df61 250 $basename = substr( $basename, 0, $max_name )
96844cae 251 if length( $basename ) > $max_name;
252 my $name = $type ? "${type}_$basename" : $basename;
253
254 if ( $basename ne $basename_orig and $critical ) {
255 my $show_type = $type ? "+'$type'" : "";
256 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
257 "character limit to make '$name'\n" if $WARN;
258 $truncated{ $basename_orig } = $name;
259 }
260
261 $scope ||= \%global_names;
262 if ( my $prev = $scope->{ $name } ) {
263 my $name_orig = $name;
264 $name .= sprintf( "%02d", ++$prev );
ea93df61 265 substr($name, $max_id_length - 3) = "00"
96844cae 266 if length( $name ) > $max_id_length;
267
268 warn "The name '$name_orig' has been changed to ",
269 "'$name' to make it unique.\n" if $WARN;
270
271 $scope->{ $name_orig }++;
f8f0253c 272 }
96844cae 273
274 $scope->{ $name }++;
275 return $name;
276}
277
e83ad71c 278sub is_geometry
279{
ea93df61 280 my $field = shift;
281 return 1 if $field->data_type eq 'geometry';
140a1dad 282}
283
284sub is_geography
285{
286 my $field = shift;
287 return 1 if $field->data_type eq 'geography';
e83ad71c 288}
289
ea93df61 290sub create_table
bfb5a568 291{
292 my ($table, $options) = @_;
293
f82112a3 294 my $generator = _generator($options);
bfb5a568 295 my $no_comments = $options->{no_comments} || 0;
296 my $add_drop_table = $options->{add_drop_table} || 0;
5342f5c1 297 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 298 my $type_defs = $options->{type_defs} || {};
bfb5a568 299
3406fd5b 300 my $table_name = $table->name or next;
f82112a3 301 my $table_name_qt = $generator->quote($table_name);
bfb5a568 302
124b192c 303# print STDERR "$table_name table_name\n";
79f55d7e 304 my ( @comments, @field_defs, @sequence_defs, @constraint_defs, @fks );
bfb5a568 305
f82112a3 306 push @comments, "--\n-- Table: $table_name\n--\n" unless $no_comments;
bfb5a568 307
308 if ( $table->comments and !$no_comments ){
309 my $c = "-- Comments: \n-- ";
310 $c .= join "\n-- ", $table->comments;
7ed7402c 311 $c .= "\n--\n";
bfb5a568 312 push @comments, $c;
313 }
314
315 #
316 # Fields
317 #
318 my %field_name_scope;
319 for my $field ( $table->get_fields ) {
f82112a3 320 push @field_defs, create_field($field, {
321 generator => $generator,
322 postgres_version => $postgres_version,
323 type_defs => $type_defs,
324 constraint_defs => \@constraint_defs,
325 });
bfb5a568 326 }
327
328 #
329 # Index Declarations
330 #
331 my @index_defs = ();
332 # my $idx_name_default;
333 for my $index ( $table->get_indices ) {
f82112a3 334 my ($idef, $constraints) = create_index($index, {
335 generator => $generator,
336 });
7ed7402c 337 $idef and push @index_defs, $idef;
bfb5a568 338 push @constraint_defs, @$constraints;
339 }
340
341 #
342 # Table constraints
343 #
344 my $c_name_default;
345 for my $c ( $table->get_constraints ) {
f82112a3 346 my ($cdefs, $fks) = create_constraint($c, {
347 generator => $generator,
348 });
bfb5a568 349 push @constraint_defs, @$cdefs;
350 push @fks, @$fks;
351 }
352
3e98f7d9 353
354 my $temporary = "";
355
aa068030 356 if(exists $table->extra->{temporary}) {
357 $temporary = $table->extra->{temporary} ? "TEMPORARY " : "";
ea93df61 358 }
3e98f7d9 359
bfb5a568 360 my $create_statement;
361 $create_statement = join("\n", @comments);
5342f5c1 362 if ($add_drop_table) {
7b4b17aa 363 if ($postgres_version >= 8.002) {
f82112a3 364 $create_statement .= "DROP TABLE IF EXISTS $table_name_qt CASCADE;\n";
5342f5c1 365 } else {
f82112a3 366 $create_statement .= "DROP TABLE $table_name_qt CASCADE;\n";
5342f5c1 367 }
368 }
f82112a3 369 $create_statement .= "CREATE ${temporary}TABLE $table_name_qt (\n" .
bfb5a568 370 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
3406fd5b 371 "\n)"
bfb5a568 372 ;
3406fd5b 373 $create_statement .= @index_defs ? ';' : q{};
374 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
375 . join(";\n", @index_defs);
bfb5a568 376
ea93df61 377 #
378 # Geometry
379 #
380 if(grep { is_geometry($_) } $table->get_fields){
e83ad71c 381 $create_statement .= ";";
140a1dad 382 my @geometry_columns;
383 foreach my $col ($table->get_fields) { push(@geometry_columns,$col) if is_geometry($col); }
ea93df61 384 $create_statement .= "\n".join("\n", map{ drop_geometry_column($_) } @geometry_columns) if $options->{add_drop_table};
385 $create_statement .= "\n".join("\n", map{ add_geometry_column($_) } @geometry_columns);
386 }
e83ad71c 387
08d91aad 388 return $create_statement, \@fks;
bfb5a568 389}
390
296c2701 391sub create_view {
392 my ($view, $options) = @_;
f82112a3 393 my $generator = _generator($options);
0e758018 394 my $postgres_version = $options->{postgres_version} || 0;
a25ac5d2 395 my $add_drop_view = $options->{add_drop_view};
296c2701 396
397 my $view_name = $view->name;
398 debug("PKG: Looking at view '${view_name}'\n");
399
400 my $create = '';
2230ed2a 401 $create .= "--\n-- View: " . $generator->quote($view_name) . "\n--\n"
296c2701 402 unless $options->{no_comments};
0e758018 403 if ($add_drop_view) {
404 if ($postgres_version >= 8.002) {
2230ed2a 405 $create .= "DROP VIEW IF EXISTS " . $generator->quote($view_name) . ";\n";
0e758018 406 } else {
2230ed2a 407 $create .= "DROP VIEW " . $generator->quote($view_name) . ";\n";
0e758018 408 }
409 }
296c2701 410 $create .= 'CREATE';
296c2701 411
412 my $extra = $view->extra;
413 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
2230ed2a 414 $create .= " VIEW " . $generator->quote($view_name);
296c2701 415
416 if ( my @fields = $view->fields ) {
2230ed2a 417 my $field_list = join ', ', map { $generator->quote($_) } @fields;
296c2701 418 $create .= " ( ${field_list} )";
419 }
420
421 if ( my $sql = $view->sql ) {
f59b2c0e 422 $create .= " AS\n ${sql}\n";
296c2701 423 }
424
425 if ( $extra->{check_option} ) {
426 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
427 }
428
296c2701 429 return $create;
430}
431
ea93df61 432{
bfb5a568 433
434 my %field_name_scope;
435
436 sub create_field
437 {
438 my ($field, $options) = @_;
439
f82112a3 440 my $generator = _generator($options);
bfb5a568 441 my $table_name = $field->table->name;
442 my $constraint_defs = $options->{constraint_defs} || [];
5342f5c1 443 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 444 my $type_defs = $options->{type_defs} || {};
bfb5a568 445
446 $field_name_scope{$table_name} ||= {};
912e67a1 447 my $field_name = $field->name;
ea93df61 448 my $field_comments = $field->comments
449 ? "-- " . $field->comments . "\n "
bfb5a568 450 : '';
451
2230ed2a 452 my $field_def = $field_comments . $generator->quote($field_name);
bfb5a568 453
454 #
455 # Datatype
456 #
457 my @size = $field->size;
458 my $data_type = lc $field->data_type;
459 my %extra = $field->extra;
460 my $list = $extra{'list'} || [];
1868ddbe 461 my $commalist = join( ', ', map { __PACKAGE__->_quote_string($_) } @$list );
bfb5a568 462
7b4b17aa 463 if ($postgres_version >= 8.003 && $field->data_type eq 'enum') {
79f55d7e 464 my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
5342f5c1 465 $field_def .= ' '. $type_name;
d25db732 466 my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
79f55d7e 467 "CREATE TYPE $type_name AS ENUM ($commalist)";
468 if (! exists $type_defs->{$type_name} ) {
469 $type_defs->{$type_name} = $new_type_def;
470 } elsif ( $type_defs->{$type_name} ne $new_type_def ) {
471 die "Attempted to redefine type name '$type_name' as a different type.\n";
472 }
5342f5c1 473 } else {
474 $field_def .= ' '. convert_datatype($field);
475 }
bfb5a568 476
477 #
ea93df61 478 # Default value
bfb5a568 479 #
1868ddbe 480 __PACKAGE__->_apply_default_value(
06baeb21 481 $field,
482 \$field_def,
483 [
484 'NULL' => \'NULL',
485 'now()' => 'now()',
486 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
487 ],
488 );
bfb5a568 489
490 #
491 # Not null constraint
492 #
493 $field_def .= ' NOT NULL' unless $field->is_nullable;
494
ea93df61 495 #
496 # Geometry constraints
497 #
498 if(is_geometry($field)){
499 foreach ( create_geometry_constraints($field) ) {
f82112a3 500 my ($cdefs, $fks) = create_constraint($_, {
501 generator => $generator,
502 });
ea93df61 503 push @$constraint_defs, @$cdefs;
504 push @$fks, @$fks;
505 }
e83ad71c 506 }
ea93df61 507
bfb5a568 508 return $field_def;
509 }
510}
511
e83ad71c 512sub create_geometry_constraints{
ea93df61 513 my $field = shift;
514
515 my @constraints;
516 push @constraints, SQL::Translator::Schema::Constraint->new(
517 name => "enforce_dims_".$field->name,
aa068030 518 expression => "(ST_NDims($field) = ".$field->extra->{dimensions}.")",
ea93df61 519 table => $field->table,
520 type => CHECK_C,
521 );
522
523 push @constraints, SQL::Translator::Schema::Constraint->new(
524 name => "enforce_srid_".$field->name,
aa068030 525 expression => "(ST_SRID($field) = ".$field->extra->{srid}.")",
ea93df61 526 table => $field->table,
527 type => CHECK_C,
528 );
529 push @constraints, SQL::Translator::Schema::Constraint->new(
530 name => "enforce_geotype_".$field->name,
aa068030 531 expression => "(GeometryType($field) = '".$field->extra->{geometry_type}."'::text OR $field IS NULL)",
ea93df61 532 table => $field->table,
533 type => CHECK_C,
534 );
535
536 return @constraints;
e83ad71c 537}
538
892573f2 539sub create_index
540{
541 my ($index, $options) = @_;
bfb5a568 542
f82112a3 543 my $generator = _generator($options);
892573f2 544 my $table_name = $index->table->name;
bfb5a568 545
892573f2 546 my ($index_def, @constraint_defs);
bfb5a568 547
681dc480 548 my $name
549 = $index->name
550 || join('_', $table_name, 'idx', ++$index_name{ $table_name });
bfb5a568 551
892573f2 552 my $type = $index->type || NORMAL;
5f31ed66 553 my @fields = $index->fields;
20530f39 554 return unless @fields;
bfb5a568 555
2230ed2a 556 my $def_start = 'CONSTRAINT ' . $generator->quote($name) . ' ';
557 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
892573f2 558 if ( $type eq PRIMARY_KEY ) {
5f31ed66 559 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 560 }
561 elsif ( $type eq UNIQUE ) {
5f31ed66 562 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 563 }
564 elsif ( $type eq NORMAL ) {
ea93df61 565 $index_def =
2230ed2a 566 'CREATE INDEX ' . $generator->quote($name) . ' on ' . $generator->quote($table_name) . ' ' . $field_names
ea93df61 567 ;
892573f2 568 }
569 else {
570 warn "Unknown index type ($type) on table $table_name.\n"
571 if $WARN;
124b192c 572 }
bfb5a568 573
892573f2 574 return $index_def, \@constraint_defs;
575}
bfb5a568 576
892573f2 577sub create_constraint
578{
579 my ($c, $options) = @_;
bfb5a568 580
f82112a3 581 my $generator = _generator($options);
892573f2 582 my $table_name = $c->table->name;
583 my (@constraint_defs, @fks);
bfb5a568 584
892573f2 585 my $name = $c->name || '';
bfb5a568 586
5f31ed66 587 my @fields = grep { defined } $c->fields;
892573f2 588
5f31ed66 589 my @rfields = grep { defined } $c->reference_fields;
892573f2 590
591 next if !@fields && $c->type ne CHECK_C;
2230ed2a 592 my $def_start = $name ? 'CONSTRAINT ' . $generator->quote($name) . ' ' : '';
593 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ( $generator->quote($_) ) } @fields)) . ')';
892573f2 594 if ( $c->type eq PRIMARY_KEY ) {
5f31ed66 595 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 596 }
597 elsif ( $c->type eq UNIQUE ) {
5f31ed66 598 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 599 }
600 elsif ( $c->type eq CHECK_C ) {
601 my $expression = $c->expression;
602 push @constraint_defs, "${def_start}CHECK ($expression)";
603 }
604 elsif ( $c->type eq FOREIGN_KEY ) {
2230ed2a 605 my $def .= "ALTER TABLE " . $generator->quote($table_name) . " ADD ${def_start}FOREIGN KEY $field_names"
606 . "\n REFERENCES " . $generator->quote($c->reference_table);
bfb5a568 607
892573f2 608 if ( @rfields ) {
2230ed2a 609 $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
892573f2 610 }
bfb5a568 611
892573f2 612 if ( $c->match_type ) {
ea93df61 613 $def .= ' MATCH ' .
892573f2 614 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
615 }
5342f5c1 616
892573f2 617 if ( $c->on_delete ) {
5863ad87 618 $def .= ' ON DELETE '. $c->on_delete;
892573f2 619 }
bfb5a568 620
892573f2 621 if ( $c->on_update ) {
5863ad87 622 $def .= ' ON UPDATE '. $c->on_update;
892573f2 623 }
124b192c 624
892573f2 625 if ( $c->deferrable ) {
626 $def .= ' DEFERRABLE';
124b192c 627 }
628
892573f2 629 push @fks, "$def";
124b192c 630 }
bfb5a568 631
892573f2 632 return \@constraint_defs, \@fks;
633}
634
c96cd4a8 635sub create_trigger {
636 my ($trigger,$options) = @_;
c9c8f3e1 637 my $generator = _generator($options);
c96cd4a8 638
639 my @statements;
640
c9c8f3e1 641 push @statements, sprintf( 'DROP TRIGGER IF EXISTS %s', $generator->quote($trigger->name) )
c96cd4a8 642 if $options->{add_drop_trigger};
643
644 my $scope = $trigger->scope || '';
645 $scope = " FOR EACH $scope" if $scope;
646
647 push @statements, sprintf(
648 'CREATE TRIGGER %s %s %s ON %s%s %s',
c9c8f3e1 649 $generator->quote($trigger->name),
c96cd4a8 650 $trigger->perform_action_when,
651 join( ' OR ', @{ $trigger->database_events } ),
c9c8f3e1 652 $generator->quote($trigger->on_table),
c96cd4a8 653 $scope,
654 $trigger->action,
655 );
656
657 return @statements;
658}
659
bfb5a568 660sub convert_datatype
661{
662 my ($field) = @_;
663
664 my @size = $field->size;
665 my $data_type = lc $field->data_type;
aacb3187 666 my $array = $data_type =~ s/\[\]$//;
bfb5a568 667
668 if ( $data_type eq 'enum' ) {
669# my $len = 0;
670# $len = ($len < length($_)) ? length($_) : $len for (@$list);
671# my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
ea93df61 672# push @$constraint_defs,
2230ed2a 673# 'CONSTRAINT "$chk_name" CHECK (' . $generator->quote(field_name) .
bfb5a568 674# qq[IN ($commalist))];
675 $data_type = 'character varying';
676 }
677 elsif ( $data_type eq 'set' ) {
678 $data_type = 'character varying';
679 }
680 elsif ( $field->is_auto_increment ) {
681 if ( defined $size[0] && $size[0] > 11 ) {
682 $data_type = 'bigserial';
683 }
684 else {
685 $data_type = 'serial';
686 }
687 undef @size;
688 }
689 else {
690 $data_type = defined $translate{ $data_type } ?
691 $translate{ $data_type } :
692 $data_type;
693 }
694
ad258776 695 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
bfb5a568 696 if ( defined $size[0] && $size[0] > 6 ) {
697 $size[0] = 6;
698 }
699 }
700
701 if ( $data_type eq 'integer' ) {
702 if ( defined $size[0] && $size[0] > 0) {
703 if ( $size[0] > 10 ) {
704 $data_type = 'bigint';
705 }
706 elsif ( $size[0] < 5 ) {
707 $data_type = 'smallint';
708 }
709 else {
710 $data_type = 'integer';
711 }
712 }
713 else {
714 $data_type = 'integer';
715 }
716 }
52bc2e13 717
718 my $type_with_size = join('|',
e189562c 719 'bit', 'varbit', 'character', 'bit varying', 'character varying',
90089d63 720 'time', 'timestamp', 'interval', 'numeric', 'float'
52bc2e13 721 );
722
723 if ( $data_type !~ /$type_with_size/ ) {
ea93df61 724 @size = ();
e56dabb7 725 }
bfb5a568 726
ad258776 727 if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
728 $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
729 $data_type .= $2 if(defined $2);
730 } elsif ( defined $size[0] && $size[0] > 0 ) {
e189562c 731 $data_type .= '(' . join( ',', @size ) . ')';
08d91aad 732 }
aacb3187 733 if($array)
734 {
735 $data_type .= '[]';
736 }
bfb5a568 737
140a1dad 738 #
739 # Geography
740 #
741 if($data_type eq 'geography'){
aa068030 742 $data_type .= '('.$field->extra->{geography_type}.','. $field->extra->{srid} .')'
140a1dad 743 }
744
bfb5a568 745 return $data_type;
746}
747
748
749sub alter_field
750{
751 my ($from_field, $to_field) = @_;
752
ea93df61 753 die "Can't alter field in another table"
bfb5a568 754 if($from_field->table->name ne $to_field->table->name);
755
756 my @out;
ea93df61 757
e83ad71c 758 # drop geometry column and constraints
c50d1a0a 759 push @out, drop_geometry_column($from_field) if is_geometry($from_field);
760 push @out, drop_geometry_constraints($from_field) if is_geometry($from_field);
761
762 # it's necessary to start with rename column cause this would affect
763 # all of the following statements which would be broken if do the
764 # rename later
765 # BUT: drop geometry is done before the rename, cause it work's on the
766 # $from_field directly
767 push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
768 $to_field->table->name,
769 $from_field->name,
770 $to_field->name) if($from_field->name ne $to_field->name);
771
3406fd5b 772 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
bfb5a568 773 $to_field->table->name,
774 $to_field->name) if(!$to_field->is_nullable and
775 $from_field->is_nullable);
776
90726ffd 777 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
778 $to_field->table->name,
779 $to_field->name)
780 if ( !$from_field->is_nullable and $to_field->is_nullable );
781
782
bfb5a568 783 my $from_dt = convert_datatype($from_field);
784 my $to_dt = convert_datatype($to_field);
3406fd5b 785 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
bfb5a568 786 $to_field->table->name,
787 $to_field->name,
788 $to_dt) if($to_dt ne $from_dt);
789
3406fd5b 790 my $old_default = $from_field->default_value;
791 my $new_default = $to_field->default_value;
90726ffd 792 my $default_value = $to_field->default_value;
ea93df61 793
90726ffd 794 # fixes bug where output like this was created:
795 # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
796 if(ref $default_value eq "SCALAR" ) {
797 $default_value = $$default_value;
798 } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
1868ddbe 799 $default_value = __PACKAGE__->_quote_string($default_value);
90726ffd 800 }
ea93df61 801
3406fd5b 802 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
bfb5a568 803 $to_field->table->name,
804 $to_field->name,
90726ffd 805 $default_value)
3406fd5b 806 if ( defined $new_default &&
807 (!defined $old_default || $old_default ne $new_default) );
bfb5a568 808
ea93df61 809 # fixes bug where removing the DEFAULT statement of a column
810 # would result in no change
811
812 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
90726ffd 813 $to_field->table->name,
814 $to_field->name)
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{
828 my ($new_field) = @_;
829
3406fd5b 830 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
bfb5a568 831 $new_field->table->name,
832 create_field($new_field));
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) = @_;
936 my $index_name = $index->name;
937 return "DROP INDEX $index_name";
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 ) {
949 # Already has a name, just quote it
f82112a3 950 $c_name = $generator->quote($c->name);
bc9b1c11 951 } elsif ( $c->type eq FOREIGN_KEY ) {
952 # Doesn't have a name, and is foreign key, append '_fkey'
f82112a3 953 $c_name = $generator->quote($c->table->name . '_' .
954 ($c->fields)[0] . '_fkey');
bc9b1c11 955 } elsif ( $c->type eq PRIMARY_KEY ) {
956 # Doesn't have a name, and is primary key, append '_pkey'
f82112a3 957 $c_name = $generator->quote($c->table->name . '_pkey');
bc9b1c11 958 }
959
c50d1a0a 960 return sprintf(
961 'ALTER TABLE %s DROP CONSTRAINT %s',
f82112a3 962 $generator->quote($c->table->name), $c_name
c50d1a0a 963 );
3406fd5b 964}
965
966sub alter_create_constraint {
967 my ($index, $options) = @_;
f82112a3 968 my $generator = _generator($options);
90726ffd 969 my ($defs, $fks) = create_constraint(@_);
ea93df61 970
90726ffd 971 # return if there are no constraint definitions so we don't run
972 # into output like this:
973 # ALTER TABLE users ADD ;
ea93df61 974
90726ffd 975 return unless(@{$defs} || @{$fks});
976 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
2230ed2a 977 : join( ' ', 'ALTER TABLE', $generator->quote($index->table->name),
90726ffd 978 'ADD', join(q{}, @{$defs}, @{$fks})
3406fd5b 979 );
980}
981
982sub drop_table {
983 my ($table, $options) = @_;
f82112a3 984 my $generator = _generator($options);
2230ed2a 985 my $out = "DROP TABLE " . $generator->quote($table) . " CASCADE";
ea93df61 986
e83ad71c 987 my @geometry_drops = map { drop_geometry_column($_); } grep { is_geometry($_) } $table->get_fields;
988
989 $out .= "\n".join("\n",@geometry_drops) if scalar(@geometry_drops);
990 return $out;
3406fd5b 991}
992
0c610cc8 993sub batch_alter_table {
994 my ( $table, $diff_hash, $options ) = @_;
0c610cc8 995
996 # as long as we're not renaming the table we don't need to be here
997 if ( @{$diff_hash->{rename_table}} == 0 ) {
86609eaa 998 return batch_alter_table_statements($diff_hash, $options);
0c610cc8 999 }
1000
1001 # first we need to perform drops which are on old table
86609eaa 1002 my @sql = batch_alter_table_statements($diff_hash, $options, qw(
1003 alter_drop_constraint
0c610cc8 1004 alter_drop_index
86609eaa 1005 drop_field
1006 ));
0c610cc8 1007
1008 # next comes the rename_table
1009 my $old_table = $diff_hash->{rename_table}[0][0];
1010 push @sql, rename_table( $old_table, $table, $options );
1011
1012 # for alter_field (and so also rename_field) we need to make sure old
1013 # field has table name set to new table otherwise calling alter_field dies
1014 $diff_hash->{alter_field} =
1015 [map { $_->[0]->table($table) && $_ } @{$diff_hash->{alter_field}}];
1016 $diff_hash->{rename_field} =
1017 [map { $_->[0]->table($table) && $_ } @{$diff_hash->{rename_field}}];
1018
1019 # now add everything else
86609eaa 1020 push @sql, batch_alter_table_statements($diff_hash, $options, qw(
1021 add_field
0c610cc8 1022 alter_field
1023 rename_field
1024 alter_create_index
1025 alter_create_constraint
86609eaa 1026 alter_table
1027 ));
0c610cc8 1028
1029 return @sql;
1030}
1031
f8f0253c 10321;
f8f0253c 1033
96844cae 1034# -------------------------------------------------------------------
1035# Life is full of misery, loneliness, and suffering --
1036# and it's all over much too soon.
1037# Woody Allen
1038# -------------------------------------------------------------------
f8f0253c 1039
96844cae 1040=pod
f8f0253c 1041
20770e44 1042=head1 SEE ALSO
1043
1044SQL::Translator, SQL::Translator::Producer::Oracle.
1045
f8f0253c 1046=head1 AUTHOR
1047
f997b9ab 1048Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
f8f0253c 1049
1050=cut