Whitespace
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / PostgreSQL.pm
CommitLineData
f8f0253c 1package SQL::Translator::Producer::PostgreSQL;
2
44659089 3# -------------------------------------------------------------------
4# Copyright (C) 2002-2009 SQLFairy Authors
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License as
8# published by the Free Software Foundation; version 2.
9#
10# This program is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
18# 02111-1307 USA
19# -------------------------------------------------------------------
20
96844cae 21=head1 NAME
22
23SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
24
20770e44 25=head1 SYNOPSIS
26
27 my $t = SQL::Translator->new( parser => '...', producer => 'PostgreSQL' );
28 $t->translate;
29
30=head1 DESCRIPTION
31
32Creates a DDL suitable for PostgreSQL. Very heavily based on the Oracle
33producer.
34
e83ad71c 35Now handles PostGIS Geometry and Geography data types on table definitions.
36Does not yet support PostGIS Views.
ea93df61 37
96844cae 38=cut
39
f8f0253c 40use strict;
bfb5a568 41use warnings;
681dc480 42use vars qw[ $DEBUG $WARN $VERSION ];
11ad2df9 43$VERSION = '1.59';
a25ac5d2 44$DEBUG = 0 unless defined $DEBUG;
f8f0253c 45
8d11f4cb 46use base qw(SQL::Translator::Producer);
0c43e0a1 47use SQL::Translator::Schema::Constants;
7b4b17aa 48use SQL::Translator::Utils qw(debug header_comment parse_dbms_version);
f8f0253c 49use Data::Dumper;
50
892573f2 51my ( %translate, %index_name );
bfb5a568 52my $max_id_length;
53
54BEGIN {
55
56 %translate = (
d529894e 57 #
58 # MySQL types
59 #
60 bigint => 'bigint',
4328d7bd 61 double => 'numeric',
62 decimal => 'numeric',
63 float => 'numeric',
d529894e 64 int => 'integer',
65 mediumint => 'integer',
66 smallint => 'smallint',
67 tinyint => 'smallint',
c8c17a58 68 char => 'character',
da8e499e 69 varchar => 'character varying',
d529894e 70 longtext => 'text',
71 mediumtext => 'text',
72 text => 'text',
73 tinytext => 'text',
74 tinyblob => 'bytea',
75 blob => 'bytea',
76 mediumblob => 'bytea',
77 longblob => 'bytea',
da8e499e 78 enum => 'character varying',
79 set => 'character varying',
d529894e 80 date => 'date',
81 datetime => 'timestamp',
e56dabb7 82 time => 'time',
d529894e 83 timestamp => 'timestamp',
84 year => 'date',
85
86 #
87 # Oracle types
88 #
96844cae 89 number => 'integer',
c8c17a58 90 char => 'character',
da8e499e 91 varchar2 => 'character varying',
96844cae 92 long => 'text',
93 CLOB => 'bytea',
94 date => 'date',
95
96 #
97 # Sybase types
98 #
99 int => 'integer',
100 money => 'money',
da8e499e 101 varchar => 'character varying',
96844cae 102 datetime => 'timestamp',
103 text => 'text',
4328d7bd 104 real => 'numeric',
96844cae 105 comment => 'text',
106 bit => 'bit',
107 tinyint => 'smallint',
4328d7bd 108 float => 'numeric',
d529894e 109);
110
bfb5a568 111 $max_id_length = 62;
112}
96844cae 113my %reserved = map { $_, 1 } qw[
ea93df61 114 ALL ANALYSE ANALYZE AND ANY AS ASC
96844cae 115 BETWEEN BINARY BOTH
116 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
ea93df61 117 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
96844cae 118 DEFAULT DEFERRABLE DESC DISTINCT DO
119 ELSE END EXCEPT
ea93df61 120 FALSE FOR FOREIGN FREEZE FROM FULL
121 GROUP HAVING
122 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
123 JOIN LEADING LEFT LIKE LIMIT
96844cae 124 NATURAL NEW NOT NOTNULL NULL
125 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
ea93df61 126 PRIMARY PUBLIC REFERENCES RIGHT
127 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
96844cae 128 UNION UNIQUE USER USING VERBOSE WHEN WHERE
129];
d529894e 130
bfb5a568 131# my $max_id_length = 62;
96844cae 132my %used_identifiers = ();
133my %global_names;
96844cae 134my %truncated;
135
136=pod
137
138=head1 PostgreSQL Create Table Syntax
139
140 CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
141 { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
142 | table_constraint } [, ... ]
143 )
144 [ INHERITS ( parent_table [, ... ] ) ]
145 [ WITH OIDS | WITHOUT OIDS ]
146
147where column_constraint is:
148
149 [ CONSTRAINT constraint_name ]
150 { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
151 CHECK (expression) |
152 REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
153 [ ON DELETE action ] [ ON UPDATE action ] }
154 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
155
156and table_constraint is:
157
158 [ CONSTRAINT constraint_name ]
159 { UNIQUE ( column_name [, ... ] ) |
160 PRIMARY KEY ( column_name [, ... ] ) |
161 CHECK ( expression ) |
162 FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
163 [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
164 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
165
da8e499e 166=head1 Create Index Syntax
167
168 CREATE [ UNIQUE ] INDEX index_name ON table
169 [ USING acc_method ] ( column [ ops_name ] [, ...] )
170 [ WHERE predicate ]
171 CREATE [ UNIQUE ] INDEX index_name ON table
172 [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
173 [ WHERE predicate ]
174
96844cae 175=cut
f8f0253c 176
96844cae 177# -------------------------------------------------------------------
f8f0253c 178sub produce {
e189562c 179 my $translator = shift;
180 local $DEBUG = $translator->debug;
181 local $WARN = $translator->show_warnings;
182 my $no_comments = $translator->no_comments;
183 my $add_drop_table = $translator->add_drop_table;
184 my $schema = $translator->schema;
185 my $pargs = $translator->producer_args;
7b4b17aa 186 my $postgres_version = parse_dbms_version(
187 $pargs->{postgres_version}, 'perl'
188 );
96844cae 189
e189562c 190 my $qt = $translator->quote_table_names ? q{"} : q{};
191 my $qf = $translator->quote_field_names ? q{"} : q{};
ea93df61 192
bf75adec 193 my @output;
194 push @output, header_comment unless ($no_comments);
96844cae 195
08d91aad 196 my (@table_defs, @fks);
79f55d7e 197 my %type_defs;
0c43e0a1 198 for my $table ( $schema->get_tables ) {
08d91aad 199
ea93df61 200 my ($table_def, $fks) = create_table($table, {
e189562c 201 quote_table_names => $qt,
202 quote_field_names => $qf,
203 no_comments => $no_comments,
204 postgres_version => $postgres_version,
205 add_drop_table => $add_drop_table,
79f55d7e 206 type_defs => \%type_defs,
e189562c 207 });
208
08d91aad 209 push @table_defs, $table_def;
210 push @fks, @$fks;
da8e499e 211 }
212
296c2701 213 for my $view ( $schema->get_views ) {
214 push @table_defs, create_view($view, {
0e758018 215 postgres_version => $postgres_version,
a25ac5d2 216 add_drop_view => $add_drop_table,
296c2701 217 quote_table_names => $qt,
218 quote_field_names => $qf,
219 no_comments => $no_comments,
220 });
221 }
222
79f55d7e 223 push @output, map { "$_;\n\n" } values %type_defs;
bf75adec 224 push @output, map { "$_;\n\n" } @table_defs;
08d91aad 225 if ( @fks ) {
bf75adec 226 push @output, "--\n-- Foreign Key Definitions\n--\n\n" unless $no_comments;
227 push @output, map { "$_;\n\n" } @fks;
08d91aad 228 }
021dbce8 229
da8e499e 230 if ( $WARN ) {
231 if ( %truncated ) {
232 warn "Truncated " . keys( %truncated ) . " names:\n";
233 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
234 }
f8f0253c 235 }
236
bf75adec 237 return wantarray
238 ? @output
239 : join ('', @output);
f8f0253c 240}
241
96844cae 242# -------------------------------------------------------------------
243sub mk_name {
ea93df61 244 my $basename = shift || '';
245 my $type = shift || '';
246 my $scope = shift || '';
0c43e0a1 247 my $critical = shift || '';
96844cae 248 my $basename_orig = $basename;
bfb5a568 249# my $max_id_length = 62;
ea93df61 250 my $max_name = $type
251 ? $max_id_length - (length($type) + 1)
2ad4c2c8 252 : $max_id_length;
ea93df61 253 $basename = substr( $basename, 0, $max_name )
96844cae 254 if length( $basename ) > $max_name;
255 my $name = $type ? "${type}_$basename" : $basename;
256
257 if ( $basename ne $basename_orig and $critical ) {
258 my $show_type = $type ? "+'$type'" : "";
259 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
260 "character limit to make '$name'\n" if $WARN;
261 $truncated{ $basename_orig } = $name;
262 }
263
264 $scope ||= \%global_names;
265 if ( my $prev = $scope->{ $name } ) {
266 my $name_orig = $name;
267 $name .= sprintf( "%02d", ++$prev );
ea93df61 268 substr($name, $max_id_length - 3) = "00"
96844cae 269 if length( $name ) > $max_id_length;
270
271 warn "The name '$name_orig' has been changed to ",
272 "'$name' to make it unique.\n" if $WARN;
273
274 $scope->{ $name_orig }++;
f8f0253c 275 }
96844cae 276
277 $scope->{ $name }++;
278 return $name;
279}
280
e83ad71c 281sub is_geometry
282{
ea93df61 283 my $field = shift;
284 return 1 if $field->data_type eq 'geometry';
140a1dad 285}
286
287sub is_geography
288{
289 my $field = shift;
290 return 1 if $field->data_type eq 'geography';
e83ad71c 291}
292
ea93df61 293sub create_table
bfb5a568 294{
295 my ($table, $options) = @_;
296
297 my $qt = $options->{quote_table_names} || '';
298 my $qf = $options->{quote_field_names} || '';
299 my $no_comments = $options->{no_comments} || 0;
300 my $add_drop_table = $options->{add_drop_table} || 0;
5342f5c1 301 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 302 my $type_defs = $options->{type_defs} || {};
bfb5a568 303
3406fd5b 304 my $table_name = $table->name or next;
3406fd5b 305 my ( $fql_tbl_name ) = ( $table_name =~ s/\W(.*)$// ) ? $1 : q{};
306 my $table_name_ur = $qt ? $table_name
5f31ed66 307 : $fql_tbl_name ? join('.', $table_name, $fql_tbl_name)
308 : $table_name;
bfb5a568 309 $table->name($table_name_ur);
310
124b192c 311# print STDERR "$table_name table_name\n";
79f55d7e 312 my ( @comments, @field_defs, @sequence_defs, @constraint_defs, @fks );
bfb5a568 313
7ed7402c 314 push @comments, "--\n-- Table: $table_name_ur\n--\n" unless $no_comments;
bfb5a568 315
316 if ( $table->comments and !$no_comments ){
317 my $c = "-- Comments: \n-- ";
318 $c .= join "\n-- ", $table->comments;
7ed7402c 319 $c .= "\n--\n";
bfb5a568 320 push @comments, $c;
321 }
322
323 #
324 # Fields
325 #
326 my %field_name_scope;
327 for my $field ( $table->get_fields ) {
328 push @field_defs, create_field($field, { quote_table_names => $qt,
329 quote_field_names => $qf,
330 table_name => $table_name_ur,
5342f5c1 331 postgres_version => $postgres_version,
79f55d7e 332 type_defs => $type_defs,
bfb5a568 333 constraint_defs => \@constraint_defs,});
334 }
335
336 #
337 # Index Declarations
338 #
339 my @index_defs = ();
340 # my $idx_name_default;
341 for my $index ( $table->get_indices ) {
342 my ($idef, $constraints) = create_index($index,
ea93df61 343 {
bfb5a568 344 quote_field_names => $qf,
345 quote_table_names => $qt,
346 table_name => $table_name,
347 });
7ed7402c 348 $idef and push @index_defs, $idef;
bfb5a568 349 push @constraint_defs, @$constraints;
350 }
351
352 #
353 # Table constraints
354 #
355 my $c_name_default;
356 for my $c ( $table->get_constraints ) {
ea93df61 357 my ($cdefs, $fks) = create_constraint($c,
358 {
bfb5a568 359 quote_field_names => $qf,
360 quote_table_names => $qt,
361 table_name => $table_name,
362 });
363 push @constraint_defs, @$cdefs;
364 push @fks, @$fks;
365 }
366
3e98f7d9 367
368 my $temporary = "";
369
370 if(exists $table->{extra}{temporary}) {
371 $temporary = $table->{extra}{temporary} ? "TEMPORARY " : "";
ea93df61 372 }
3e98f7d9 373
bfb5a568 374 my $create_statement;
375 $create_statement = join("\n", @comments);
5342f5c1 376 if ($add_drop_table) {
7b4b17aa 377 if ($postgres_version >= 8.002) {
5342f5c1 378 $create_statement .= qq[DROP TABLE IF EXISTS $qt$table_name_ur$qt CASCADE;\n];
5342f5c1 379 } else {
380 $create_statement .= qq[DROP TABLE $qt$table_name_ur$qt CASCADE;\n];
381 }
382 }
3e98f7d9 383 $create_statement .= qq[CREATE ${temporary}TABLE $qt$table_name_ur$qt (\n].
bfb5a568 384 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
3406fd5b 385 "\n)"
bfb5a568 386 ;
3406fd5b 387 $create_statement .= @index_defs ? ';' : q{};
388 $create_statement .= ( $create_statement =~ /;$/ ? "\n" : q{} )
389 . join(";\n", @index_defs);
bfb5a568 390
ea93df61 391 #
392 # Geometry
393 #
394 if(grep { is_geometry($_) } $table->get_fields){
e83ad71c 395 $create_statement .= ";";
140a1dad 396 my @geometry_columns;
397 foreach my $col ($table->get_fields) { push(@geometry_columns,$col) if is_geometry($col); }
ea93df61 398 $create_statement .= "\n".join("\n", map{ drop_geometry_column($_) } @geometry_columns) if $options->{add_drop_table};
399 $create_statement .= "\n".join("\n", map{ add_geometry_column($_) } @geometry_columns);
400 }
e83ad71c 401
08d91aad 402 return $create_statement, \@fks;
bfb5a568 403}
404
296c2701 405sub create_view {
406 my ($view, $options) = @_;
407 my $qt = $options->{quote_table_names} || '';
408 my $qf = $options->{quote_field_names} || '';
0e758018 409 my $postgres_version = $options->{postgres_version} || 0;
a25ac5d2 410 my $add_drop_view = $options->{add_drop_view};
296c2701 411
412 my $view_name = $view->name;
413 debug("PKG: Looking at view '${view_name}'\n");
414
415 my $create = '';
416 $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n"
417 unless $options->{no_comments};
0e758018 418 if ($add_drop_view) {
419 if ($postgres_version >= 8.002) {
420 $create .= "DROP VIEW IF EXISTS ${qt}${view_name}${qt};\n";
421 } else {
422 $create .= "DROP VIEW ${qt}${view_name}${qt};\n";
423 }
424 }
296c2701 425 $create .= 'CREATE';
296c2701 426
427 my $extra = $view->extra;
428 $create .= " TEMPORARY" if exists($extra->{temporary}) && $extra->{temporary};
429 $create .= " VIEW ${qt}${view_name}${qt}";
430
431 if ( my @fields = $view->fields ) {
432 my $field_list = join ', ', map { "${qf}${_}${qf}" } @fields;
433 $create .= " ( ${field_list} )";
434 }
435
436 if ( my $sql = $view->sql ) {
f59b2c0e 437 $create .= " AS\n ${sql}\n";
296c2701 438 }
439
440 if ( $extra->{check_option} ) {
441 $create .= ' WITH ' . uc $extra->{check_option} . ' CHECK OPTION';
442 }
443
296c2701 444 return $create;
445}
446
ea93df61 447{
bfb5a568 448
449 my %field_name_scope;
450
451 sub create_field
452 {
453 my ($field, $options) = @_;
454
455 my $qt = $options->{quote_table_names} || '';
456 my $qf = $options->{quote_field_names} || '';
457 my $table_name = $field->table->name;
458 my $constraint_defs = $options->{constraint_defs} || [];
5342f5c1 459 my $postgres_version = $options->{postgres_version} || 0;
79f55d7e 460 my $type_defs = $options->{type_defs} || {};
bfb5a568 461
462 $field_name_scope{$table_name} ||= {};
912e67a1 463 my $field_name = $field->name;
ea93df61 464 my $field_comments = $field->comments
465 ? "-- " . $field->comments . "\n "
bfb5a568 466 : '';
467
5f31ed66 468 my $field_def = $field_comments.qq[$qf$field_name$qf];
bfb5a568 469
470 #
471 # Datatype
472 #
473 my @size = $field->size;
474 my $data_type = lc $field->data_type;
475 my %extra = $field->extra;
476 my $list = $extra{'list'} || [];
477 # todo deal with embedded quotes
478 my $commalist = join( ', ', map { qq['$_'] } @$list );
bfb5a568 479
7b4b17aa 480 if ($postgres_version >= 8.003 && $field->data_type eq 'enum') {
79f55d7e 481 my $type_name = $extra{'custom_type_name'} || $field->table->name . '_' . $field->name . '_type';
5342f5c1 482 $field_def .= ' '. $type_name;
d25db732 483 my $new_type_def = "DROP TYPE IF EXISTS $type_name CASCADE;\n" .
79f55d7e 484 "CREATE TYPE $type_name AS ENUM ($commalist)";
485 if (! exists $type_defs->{$type_name} ) {
486 $type_defs->{$type_name} = $new_type_def;
487 } elsif ( $type_defs->{$type_name} ne $new_type_def ) {
488 die "Attempted to redefine type name '$type_name' as a different type.\n";
489 }
5342f5c1 490 } else {
491 $field_def .= ' '. convert_datatype($field);
492 }
bfb5a568 493
494 #
ea93df61 495 # Default value
bfb5a568 496 #
06baeb21 497 SQL::Translator::Producer->_apply_default_value(
498 $field,
499 \$field_def,
500 [
501 'NULL' => \'NULL',
502 'now()' => 'now()',
503 'CURRENT_TIMESTAMP' => 'CURRENT_TIMESTAMP',
504 ],
505 );
bfb5a568 506
507 #
508 # Not null constraint
509 #
510 $field_def .= ' NOT NULL' unless $field->is_nullable;
511
ea93df61 512 #
513 # Geometry constraints
514 #
515 if(is_geometry($field)){
516 foreach ( create_geometry_constraints($field) ) {
517 my ($cdefs, $fks) = create_constraint($_,
518 {
519 quote_field_names => $qf,
520 quote_table_names => $qt,
521 table_name => $table_name,
522 });
523 push @$constraint_defs, @$cdefs;
524 push @$fks, @$fks;
525 }
e83ad71c 526 }
ea93df61 527
bfb5a568 528 return $field_def;
529 }
530}
531
e83ad71c 532sub create_geometry_constraints{
ea93df61 533 my $field = shift;
534
535 my @constraints;
536 push @constraints, SQL::Translator::Schema::Constraint->new(
537 name => "enforce_dims_".$field->name,
538 expression => "(ST_NDims($field) = ".$field->{extra}{dimensions}.")",
539 table => $field->table,
540 type => CHECK_C,
541 );
542
543 push @constraints, SQL::Translator::Schema::Constraint->new(
544 name => "enforce_srid_".$field->name,
545 expression => "(ST_SRID($field) = ".$field->{extra}{srid}.")",
546 table => $field->table,
547 type => CHECK_C,
548 );
549 push @constraints, SQL::Translator::Schema::Constraint->new(
550 name => "enforce_geotype_".$field->name,
551 expression => "(GeometryType($field) = '".$field->{extra}{geometry_type}."'::text OR $field IS NULL)",
552 table => $field->table,
553 type => CHECK_C,
554 );
555
556 return @constraints;
e83ad71c 557}
558
892573f2 559sub create_index
560{
561 my ($index, $options) = @_;
bfb5a568 562
892573f2 563 my $qt = $options->{quote_table_names} ||'';
564 my $qf = $options->{quote_field_names} ||'';
565 my $table_name = $index->table->name;
bfb5a568 566
892573f2 567 my ($index_def, @constraint_defs);
bfb5a568 568
681dc480 569 my $name
570 = $index->name
571 || join('_', $table_name, 'idx', ++$index_name{ $table_name });
bfb5a568 572
892573f2 573 my $type = $index->type || NORMAL;
5f31ed66 574 my @fields = $index->fields;
892573f2 575 next unless @fields;
bfb5a568 576
5f31ed66 577 my $def_start = qq[CONSTRAINT ${qf}$name${qf} ];
578 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ($qf . $_ . $qf ) } @fields)) . ')';
892573f2 579 if ( $type eq PRIMARY_KEY ) {
5f31ed66 580 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 581 }
582 elsif ( $type eq UNIQUE ) {
5f31ed66 583 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 584 }
585 elsif ( $type eq NORMAL ) {
ea93df61 586 $index_def =
5f31ed66 587 "CREATE INDEX ${qf}${name}${qf} on ${qt}${table_name}${qt} ".$field_names
ea93df61 588 ;
892573f2 589 }
590 else {
591 warn "Unknown index type ($type) on table $table_name.\n"
592 if $WARN;
124b192c 593 }
bfb5a568 594
892573f2 595 return $index_def, \@constraint_defs;
596}
bfb5a568 597
892573f2 598sub create_constraint
599{
600 my ($c, $options) = @_;
bfb5a568 601
892573f2 602 my $qf = $options->{quote_field_names} ||'';
603 my $qt = $options->{quote_table_names} ||'';
604 my $table_name = $c->table->name;
605 my (@constraint_defs, @fks);
bfb5a568 606
892573f2 607 my $name = $c->name || '';
bfb5a568 608
5f31ed66 609 my @fields = grep { defined } $c->fields;
892573f2 610
5f31ed66 611 my @rfields = grep { defined } $c->reference_fields;
892573f2 612
613 next if !@fields && $c->type ne CHECK_C;
5f31ed66 614 my $def_start = $name ? qq[CONSTRAINT ${qf}$name${qf} ] : '';
615 my $field_names = '(' . join(", ", (map { $_ =~ /\(.*\)/ ? $_ : ($qf . $_ . $qf ) } @fields)) . ')';
892573f2 616 if ( $c->type eq PRIMARY_KEY ) {
5f31ed66 617 push @constraint_defs, "${def_start}PRIMARY KEY ".$field_names;
892573f2 618 }
619 elsif ( $c->type eq UNIQUE ) {
5f31ed66 620 push @constraint_defs, "${def_start}UNIQUE " .$field_names;
892573f2 621 }
622 elsif ( $c->type eq CHECK_C ) {
623 my $expression = $c->expression;
624 push @constraint_defs, "${def_start}CHECK ($expression)";
625 }
626 elsif ( $c->type eq FOREIGN_KEY ) {
681dc480 627 my $def .= "ALTER TABLE $qt$table_name$qt ADD ${def_start}FOREIGN KEY $field_names"
628 . "\n REFERENCES " . $qt . $c->reference_table . $qt;
bfb5a568 629
892573f2 630 if ( @rfields ) {
631 $def .= ' ('.$qf . join( $qf.', '.$qf, @rfields ) . $qf.')';
632 }
bfb5a568 633
892573f2 634 if ( $c->match_type ) {
ea93df61 635 $def .= ' MATCH ' .
892573f2 636 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
637 }
5342f5c1 638
892573f2 639 if ( $c->on_delete ) {
5863ad87 640 $def .= ' ON DELETE '. $c->on_delete;
892573f2 641 }
bfb5a568 642
892573f2 643 if ( $c->on_update ) {
5863ad87 644 $def .= ' ON UPDATE '. $c->on_update;
892573f2 645 }
124b192c 646
892573f2 647 if ( $c->deferrable ) {
648 $def .= ' DEFERRABLE';
124b192c 649 }
650
892573f2 651 push @fks, "$def";
124b192c 652 }
bfb5a568 653
892573f2 654 return \@constraint_defs, \@fks;
655}
656
bfb5a568 657sub convert_datatype
658{
659 my ($field) = @_;
660
661 my @size = $field->size;
662 my $data_type = lc $field->data_type;
aacb3187 663 my $array = $data_type =~ s/\[\]$//;
bfb5a568 664
665 if ( $data_type eq 'enum' ) {
666# my $len = 0;
667# $len = ($len < length($_)) ? length($_) : $len for (@$list);
668# my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
ea93df61 669# push @$constraint_defs,
3406fd5b 670# qq[CONSTRAINT "$chk_name" CHECK ($qf$field_name$qf ].
bfb5a568 671# qq[IN ($commalist))];
672 $data_type = 'character varying';
673 }
674 elsif ( $data_type eq 'set' ) {
675 $data_type = 'character varying';
676 }
677 elsif ( $field->is_auto_increment ) {
678 if ( defined $size[0] && $size[0] > 11 ) {
679 $data_type = 'bigserial';
680 }
681 else {
682 $data_type = 'serial';
683 }
684 undef @size;
685 }
686 else {
687 $data_type = defined $translate{ $data_type } ?
688 $translate{ $data_type } :
689 $data_type;
690 }
691
ad258776 692 if ( $data_type =~ /^time/i || $data_type =~ /^interval/i ) {
bfb5a568 693 if ( defined $size[0] && $size[0] > 6 ) {
694 $size[0] = 6;
695 }
696 }
697
698 if ( $data_type eq 'integer' ) {
699 if ( defined $size[0] && $size[0] > 0) {
700 if ( $size[0] > 10 ) {
701 $data_type = 'bigint';
702 }
703 elsif ( $size[0] < 5 ) {
704 $data_type = 'smallint';
705 }
706 else {
707 $data_type = 'integer';
708 }
709 }
710 else {
711 $data_type = 'integer';
712 }
713 }
52bc2e13 714
715 my $type_with_size = join('|',
e189562c 716 'bit', 'varbit', 'character', 'bit varying', 'character varying',
c3bddac9 717 'time', 'timestamp', 'interval', 'numeric'
52bc2e13 718 );
719
720 if ( $data_type !~ /$type_with_size/ ) {
ea93df61 721 @size = ();
e56dabb7 722 }
bfb5a568 723
ad258776 724 if (defined $size[0] && $size[0] > 0 && $data_type =~ /^time/i ) {
725 $data_type =~ s/^(time.*?)( with.*)?$/$1($size[0])/;
726 $data_type .= $2 if(defined $2);
727 } elsif ( defined $size[0] && $size[0] > 0 ) {
e189562c 728 $data_type .= '(' . join( ',', @size ) . ')';
08d91aad 729 }
aacb3187 730 if($array)
731 {
732 $data_type .= '[]';
733 }
bfb5a568 734
140a1dad 735 #
736 # Geography
737 #
738 if($data_type eq 'geography'){
739 $data_type .= '('.$field->{extra}{geography_type}.','. $field->{extra}{srid} .')'
740 }
741
bfb5a568 742 return $data_type;
743}
744
745
746sub alter_field
747{
748 my ($from_field, $to_field) = @_;
749
ea93df61 750 die "Can't alter field in another table"
bfb5a568 751 if($from_field->table->name ne $to_field->table->name);
752
753 my @out;
ea93df61 754
e83ad71c 755 # drop geometry column and constraints
c50d1a0a 756 push @out, drop_geometry_column($from_field) if is_geometry($from_field);
757 push @out, drop_geometry_constraints($from_field) if is_geometry($from_field);
758
759 # it's necessary to start with rename column cause this would affect
760 # all of the following statements which would be broken if do the
761 # rename later
762 # BUT: drop geometry is done before the rename, cause it work's on the
763 # $from_field directly
764 push @out, sprintf('ALTER TABLE %s RENAME COLUMN %s TO %s',
765 $to_field->table->name,
766 $from_field->name,
767 $to_field->name) if($from_field->name ne $to_field->name);
768
3406fd5b 769 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET NOT NULL',
bfb5a568 770 $to_field->table->name,
771 $to_field->name) if(!$to_field->is_nullable and
772 $from_field->is_nullable);
773
90726ffd 774 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL',
775 $to_field->table->name,
776 $to_field->name)
777 if ( !$from_field->is_nullable and $to_field->is_nullable );
778
779
bfb5a568 780 my $from_dt = convert_datatype($from_field);
781 my $to_dt = convert_datatype($to_field);
3406fd5b 782 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s TYPE %s',
bfb5a568 783 $to_field->table->name,
784 $to_field->name,
785 $to_dt) if($to_dt ne $from_dt);
786
3406fd5b 787 my $old_default = $from_field->default_value;
788 my $new_default = $to_field->default_value;
90726ffd 789 my $default_value = $to_field->default_value;
ea93df61 790
90726ffd 791 # fixes bug where output like this was created:
792 # ALTER TABLE users ALTER COLUMN column SET DEFAULT ThisIsUnescaped;
793 if(ref $default_value eq "SCALAR" ) {
794 $default_value = $$default_value;
795 } elsif( defined $default_value && $to_dt =~ /^(character|text)/xsmi ) {
796 $default_value =~ s/'/''/xsmg;
797 $default_value = q(') . $default_value . q(');
798 }
ea93df61 799
3406fd5b 800 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
bfb5a568 801 $to_field->table->name,
802 $to_field->name,
90726ffd 803 $default_value)
3406fd5b 804 if ( defined $new_default &&
805 (!defined $old_default || $old_default ne $new_default) );
bfb5a568 806
ea93df61 807 # fixes bug where removing the DEFAULT statement of a column
808 # would result in no change
809
810 push @out, sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT',
90726ffd 811 $to_field->table->name,
812 $to_field->name)
813 if ( !defined $new_default && defined $old_default );
ea93df61 814
815 # add geometry column and contraints
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{
826 my ($new_field) = @_;
827
3406fd5b 828 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
bfb5a568 829 $new_field->table->name,
830 create_field($new_field));
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
841 my $qt = $options->{quote_table_names} ||'';
842 my $qf = $options->{quote_field_names} ||'';
bfb5a568 843
3406fd5b 844 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
c50d1a0a 845 $qt . $old_field->table->name . $qt,
846 $qf . $old_field->name . $qf);
847 $out .= "\n".drop_geometry_column($old_field) if is_geometry($old_field);
ea93df61 848 return $out;
bfb5a568 849}
850
e83ad71c 851sub add_geometry_column{
ea93df61 852 my ($field,$options) = @_;
853
854 my $out = sprintf("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});
e83ad71c 862 return $out;
863}
864
865sub drop_geometry_column
866{
ea93df61 867 my $field = shift;
868
869 my $out = sprintf("DELETE FROM geometry_columns WHERE f_table_schema = '%s' AND f_table_name = '%s' AND f_geometry_column = '%s'",
870 $field->table->schema->name,
871 $field->table->name,
872 $field->name);
e83ad71c 873 return $out;
874}
875
876sub add_geometry_constraints{
ea93df61 877 my $field = shift;
878
879 my @constraints = create_geometry_constraints($field);
880
881 my $out = join("\n", map { alter_create_constraint($_); } @constraints);
e83ad71c 882
ea93df61 883 return $out;
e83ad71c 884}
885
886sub drop_geometry_constraints{
ea93df61 887 my $field = shift;
888
889 my @constraints = create_geometry_constraints($field);
890
891 my $out = join("\n", map { alter_drop_constraint($_); } @constraints);
892
893 return $out;
e83ad71c 894}
895
3406fd5b 896sub alter_table {
897 my ($to_table, $options) = @_;
898 my $qt = $options->{quote_table_names} || '';
899 my $out = sprintf('ALTER TABLE %s %s',
900 $qt . $to_table->name . $qt,
901 $options->{alter_table_action});
e83ad71c 902 $out .= "\n".$options->{geometry_changes} if $options->{geometry_changes};
3406fd5b 903 return $out;
904}
905
906sub rename_table {
907 my ($old_table, $new_table, $options) = @_;
908 my $qt = $options->{quote_table_names} || '';
909 $options->{alter_table_action} = "RENAME TO $qt$new_table$qt";
e83ad71c 910
ea93df61 911 my @geometry_changes;
912 push @geometry_changes, map { drop_geometry_column($_); } grep { is_geometry($_) } $old_table->get_fields;
913 push @geometry_changes, map { add_geometry_column($_, { table => $new_table }); } grep { is_geometry($_) } $old_table->get_fields;
914
e83ad71c 915 $options->{geometry_changes} = join ("\n",@geometry_changes) if scalar(@geometry_changes);
ea93df61 916
3406fd5b 917 return alter_table($old_table, $options);
918}
919
920sub alter_create_index {
921 my ($index, $options) = @_;
922 my $qt = $options->{quote_table_names} || '';
923 my $qf = $options->{quote_field_names} || '';
924 my ($idef, $constraints) = create_index($index, {
925 quote_field_names => $qf,
926 quote_table_names => $qt,
927 table_name => $index->table->name,
928 });
929 return $index->type eq NORMAL ? $idef
930 : sprintf('ALTER TABLE %s ADD %s',
931 $qt . $index->table->name . $qt,
932 join(q{}, @$constraints)
933 );
934}
935
936sub alter_drop_index {
937 my ($index, $options) = @_;
938 my $index_name = $index->name;
939 return "DROP INDEX $index_name";
940}
941
942sub alter_drop_constraint {
943 my ($c, $options) = @_;
944 my $qt = $options->{quote_table_names} || '';
945 my $qc = $options->{quote_field_names} || '';
c50d1a0a 946
947 return sprintf(
948 'ALTER TABLE %s DROP CONSTRAINT %s',
949 $qt . $c->table->name . $qt,
950 # attention: Postgres has a very special naming structure
951 # for naming foreign keys, it names them uses the name of
952 # the table as prefix and fkey as suffix, concatenated by a underscore
953 $c->type eq FOREIGN_KEY
681dc480 954 ? $c->name
955 ? $qc . $c->name . $qc
956 : $qc . $c->table->name . '_' . ($c->fields)[0] . '_fkey' . $qc
c50d1a0a 957 : $qc . $c->name . $qc
958 );
3406fd5b 959}
960
961sub alter_create_constraint {
962 my ($index, $options) = @_;
963 my $qt = $options->{quote_table_names} || '';
90726ffd 964 my ($defs, $fks) = create_constraint(@_);
ea93df61 965
90726ffd 966 # return if there are no constraint definitions so we don't run
967 # into output like this:
968 # ALTER TABLE users ADD ;
ea93df61 969
90726ffd 970 return unless(@{$defs} || @{$fks});
971 return $index->type eq FOREIGN_KEY ? join(q{}, @{$fks})
3406fd5b 972 : join( ' ', 'ALTER TABLE', $qt.$index->table->name.$qt,
90726ffd 973 'ADD', join(q{}, @{$defs}, @{$fks})
3406fd5b 974 );
975}
976
977sub drop_table {
978 my ($table, $options) = @_;
979 my $qt = $options->{quote_table_names} || '';
e83ad71c 980 my $out = "DROP TABLE $qt$table$qt CASCADE";
ea93df61 981
e83ad71c 982 my @geometry_drops = map { drop_geometry_column($_); } grep { is_geometry($_) } $table->get_fields;
983
984 $out .= "\n".join("\n",@geometry_drops) if scalar(@geometry_drops);
985 return $out;
3406fd5b 986}
987
f8f0253c 9881;
f8f0253c 989
96844cae 990# -------------------------------------------------------------------
991# Life is full of misery, loneliness, and suffering --
992# and it's all over much too soon.
993# Woody Allen
994# -------------------------------------------------------------------
f8f0253c 995
96844cae 996=pod
f8f0253c 997
20770e44 998=head1 SEE ALSO
999
1000SQL::Translator, SQL::Translator::Producer::Oracle.
1001
f8f0253c 1002=head1 AUTHOR
1003
f997b9ab 1004Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
f8f0253c 1005
1006=cut