Now with check constraints!
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / PostgreSQL.pm
CommitLineData
f8f0253c 1package SQL::Translator::Producer::PostgreSQL;
2
3# -------------------------------------------------------------------
8065e024 4# $Id: PostgreSQL.pm,v 1.15 2003-08-21 18:10:14 kycl4rk Exp $
f8f0253c 5# -------------------------------------------------------------------
abfa405a 6# Copyright (C) 2003 Ken Y. Clark <kclark@cpan.org>,
7# darren chamberlain <darren@cpan.org>,
8# Chris Mungall <cjm@fruitfly.org>
f8f0253c 9#
10# This program is free software; you can redistribute it and/or
11# modify it under the terms of the GNU General Public License as
12# published by the Free Software Foundation; version 2.
13#
14# This program is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17# General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
22# 02111-1307 USA
23# -------------------------------------------------------------------
24
96844cae 25=head1 NAME
26
27SQL::Translator::Producer::PostgreSQL - PostgreSQL producer for SQL::Translator
28
29=cut
30
f8f0253c 31use strict;
96844cae 32use vars qw[ $DEBUG $WARN $VERSION ];
8065e024 33$VERSION = sprintf "%d.%02d", q$Revision: 1.15 $ =~ /(\d+)\.(\d+)/;
f8f0253c 34$DEBUG = 1 unless defined $DEBUG;
35
0c43e0a1 36use SQL::Translator::Schema::Constants;
5ee19df8 37use SQL::Translator::Utils qw(header_comment);
f8f0253c 38use Data::Dumper;
39
d529894e 40my %translate = (
41 #
42 # MySQL types
43 #
44 bigint => 'bigint',
45 double => 'double precision',
46 decimal => 'decimal',
47 float => 'double precision',
48 int => 'integer',
49 mediumint => 'integer',
50 smallint => 'smallint',
51 tinyint => 'smallint',
52 char => 'char',
da8e499e 53 varchar => 'character varying',
d529894e 54 longtext => 'text',
55 mediumtext => 'text',
56 text => 'text',
57 tinytext => 'text',
58 tinyblob => 'bytea',
59 blob => 'bytea',
60 mediumblob => 'bytea',
61 longblob => 'bytea',
da8e499e 62 enum => 'character varying',
63 set => 'character varying',
d529894e 64 date => 'date',
65 datetime => 'timestamp',
66 time => 'date',
67 timestamp => 'timestamp',
68 year => 'date',
69
70 #
71 # Oracle types
72 #
96844cae 73 number => 'integer',
74 char => 'char',
da8e499e 75 varchar2 => 'character varying',
96844cae 76 long => 'text',
77 CLOB => 'bytea',
78 date => 'date',
79
80 #
81 # Sybase types
82 #
83 int => 'integer',
84 money => 'money',
da8e499e 85 varchar => 'character varying',
96844cae 86 datetime => 'timestamp',
87 text => 'text',
88 real => 'double precision',
89 comment => 'text',
90 bit => 'bit',
91 tinyint => 'smallint',
92 float => 'double precision',
d529894e 93);
94
96844cae 95my %reserved = map { $_, 1 } qw[
96 ALL ANALYSE ANALYZE AND ANY AS ASC
97 BETWEEN BINARY BOTH
98 CASE CAST CHECK COLLATE COLUMN CONSTRAINT CROSS
99 CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER
100 DEFAULT DEFERRABLE DESC DISTINCT DO
101 ELSE END EXCEPT
102 FALSE FOR FOREIGN FREEZE FROM FULL
103 GROUP HAVING
104 ILIKE IN INITIALLY INNER INTERSECT INTO IS ISNULL
105 JOIN LEADING LEFT LIKE LIMIT
106 NATURAL NEW NOT NOTNULL NULL
107 OFF OFFSET OLD ON ONLY OR ORDER OUTER OVERLAPS
108 PRIMARY PUBLIC REFERENCES RIGHT
109 SELECT SESSION_USER SOME TABLE THEN TO TRAILING TRUE
110 UNION UNIQUE USER USING VERBOSE WHEN WHERE
111];
d529894e 112
96844cae 113my $max_id_length = 30;
114my %used_identifiers = ();
115my %global_names;
116my %unreserve;
117my %truncated;
118
119=pod
120
121=head1 PostgreSQL Create Table Syntax
122
123 CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name (
124 { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ]
125 | table_constraint } [, ... ]
126 )
127 [ INHERITS ( parent_table [, ... ] ) ]
128 [ WITH OIDS | WITHOUT OIDS ]
129
130where column_constraint is:
131
132 [ CONSTRAINT constraint_name ]
133 { NOT NULL | NULL | UNIQUE | PRIMARY KEY |
134 CHECK (expression) |
135 REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ]
136 [ ON DELETE action ] [ ON UPDATE action ] }
137 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
138
139and table_constraint is:
140
141 [ CONSTRAINT constraint_name ]
142 { UNIQUE ( column_name [, ... ] ) |
143 PRIMARY KEY ( column_name [, ... ] ) |
144 CHECK ( expression ) |
145 FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ]
146 [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] }
147 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
148
da8e499e 149=head1 Create Index Syntax
150
151 CREATE [ UNIQUE ] INDEX index_name ON table
152 [ USING acc_method ] ( column [ ops_name ] [, ...] )
153 [ WHERE predicate ]
154 CREATE [ UNIQUE ] INDEX index_name ON table
155 [ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] )
156 [ WHERE predicate ]
157
96844cae 158=cut
f8f0253c 159
96844cae 160# -------------------------------------------------------------------
f8f0253c 161sub produce {
a1d94525 162 my $translator = shift;
163 $DEBUG = $translator->debug;
164 $WARN = $translator->show_warnings;
165 my $no_comments = $translator->no_comments;
166 my $add_drop_table = $translator->add_drop_table;
167 my $schema = $translator->schema;
96844cae 168
da8e499e 169 my $output;
5ee19df8 170 $output .= header_comment unless ($no_comments);
50840472 171 my %used_index_names;
96844cae 172
0c43e0a1 173 for my $table ( $schema->get_tables ) {
174 my $table_name = $table->name or next;
da8e499e 175 $table_name = mk_name( $table_name, '', undef, 1 );
176 my $table_name_ur = unreserve($table_name);
177
0c43e0a1 178 my ( @comments, @field_defs, @sequence_defs, @constraint_defs );
96844cae 179
da8e499e 180 push @comments, "--\n-- Table: $table_name_ur\n--" unless $no_comments;
f8f0253c 181
182 #
183 # Fields
184 #
96844cae 185 my %field_name_scope;
0c43e0a1 186 for my $field ( $table->get_fields ) {
96844cae 187 my $field_name = mk_name(
0c43e0a1 188 $field->name, '', \%field_name_scope, 1
96844cae 189 );
190 my $field_name_ur = unreserve( $field_name, $table_name );
0c43e0a1 191 my $field_def = qq["$field_name_ur"];
da8e499e 192
193 #
194 # Datatype
195 #
0c43e0a1 196 my @size = $field->size;
197 my $data_type = lc $field->data_type;
198 my %extra = $field->extra;
199 my $list = $extra{'list'} || [];
4524cf01 200 # todo deal with embedded quotes
201 my $commalist = join( ', ', map { qq['$_'] } @$list );
da8e499e 202 my $seq_name;
203
204 if ( $data_type eq 'enum' ) {
205 my $len = 0;
206 $len = ($len < length($_)) ? length($_) : $len for (@$list);
4524cf01 207 my $chk_name = mk_name( $table_name.'_'.$field_name, 'chk' );
0c43e0a1 208 push @constraint_defs,
4524cf01 209 qq[CONSTRAINT $chk_name CHECK ("$field_name" IN ($commalist))];
0c43e0a1 210 $data_type = 'character varying';
da8e499e 211 }
212 elsif ( $data_type eq 'set' ) {
213 # XXX add a CHECK constraint maybe
214 # (trickier and slower, than enum :)
0c43e0a1 215# my $len = length $commalist;
216# $field_def .= " character varying($len) /* set $commalist */";
217 $data_type = 'character varying';
da8e499e 218 }
0c43e0a1 219 elsif ( $field->is_auto_increment ) {
50840472 220 if ( defined $size[0] && $size[0] > 11 ) {
221 $data_type = ' bigserial';
222 }
223 else {
224 $data_type = ' serial';
225 }
226 undef @size;
227
0c43e0a1 228# $seq_name = mk_name( $table_name.'_'.$field_name, 'sq' );
229# push @sequence_defs, qq[DROP SEQUENCE "$seq_name";];
230# push @sequence_defs, qq[CREATE SEQUENCE "$seq_name";];
da8e499e 231 }
232 else {
233 $data_type = defined $translate{ $data_type } ?
234 $translate{ $data_type } :
61aa0c9d 235 $data_type;
0c43e0a1 236 }
237
50840472 238 if ( $data_type =~ /timestamp/i ) {
239 if ( defined $size[0] && $size[0] > 13 ) {
240 $size[0] = 13;
241 }
242 }
243
244 if ( $data_type eq 'integer' ) {
245 if ( defined $size[0] ) {
246 if ( $size[0] > 10 ) { #
247 $data_type = ' bigint';
248 }
249 elsif ( $size[0] < 5 ) {
250 $data_type = ' smallint';
251 }
252 else {
253 $data_type = ' integer';
254 }
255 }
256 else {
257 $data_type = ' integer';
258 }
259 undef @size;
260 }
261
0c43e0a1 262 $field_def .= " $data_type";
263
264 if ( defined $size[0] && $size[0] > 0 ) {
265 $field_def .= '(' . join( ', ', @size ) . ')';
f8f0253c 266 }
267
da8e499e 268 #
269 # Default value
270 #
0c43e0a1 271 my $default = $field->default_value;
272 if ( defined $default ) {
273 $field_def .= sprintf( ' DEFAULT %s',
274 ( $field->is_auto_increment && $seq_name )
da8e499e 275 ? qq[nextval('"$seq_name"'::text)] :
0c43e0a1 276 ( $default =~ m/null/i ) ? 'NULL' : "'$default'"
da8e499e 277 );
278 }
f8f0253c 279
da8e499e 280 #
281 # Not null constraint
282 #
0c43e0a1 283 $field_def .= ' NOT NULL' unless $field->is_nullable;
f8f0253c 284
0c43e0a1 285 push @field_defs, $field_def;
f8f0253c 286 }
f8f0253c 287
288 #
da8e499e 289 # Index Declarations
f8f0253c 290 #
0c43e0a1 291 my @index_defs = ();
da8e499e 292 my $idx_name_default;
0c43e0a1 293 for my $index ( $table->get_indices ) {
294 my $name = $index->name || '';
295 my $type = $index->type || NORMAL;
d8dcdb7a 296 my @fields =
297 map { $_ =~ s/\(.+\)//; $_ }
298 map { unreserve( $_, $table_name ) }
0c43e0a1 299 $index->fields;
da8e499e 300 next unless @fields;
301
0c43e0a1 302 if ( $type eq PRIMARY_KEY ) {
303 $name ||= mk_name( $table_name, 'pk' );
50840472 304 $name = next_unused_name($name, \%used_index_names);
305 # how do I get next_unused_name() to do: ?
306 $used_index_names{$name} = $name;
0c43e0a1 307 push @constraint_defs, 'CONSTRAINT '.$name.' PRIMARY KEY '.
50840472 308 '("' . join( '", "', @fields ) . '")';
da8e499e 309 }
0c43e0a1 310 elsif ( $type eq UNIQUE ) {
311 $name ||= mk_name(
312 $table_name, $name || ++$idx_name_default
da8e499e 313 );
50840472 314 $name = next_unused_name($name, \%used_index_names);
315 $used_index_names{$name} = $name;
0c43e0a1 316 push @constraint_defs, 'CONSTRAINT ' . $name . ' UNIQUE ' .
50840472 317 '("' . join( '", "', @fields ) . '")';
da8e499e 318 }
0c43e0a1 319 elsif ( $type eq NORMAL ) {
320 $name ||= mk_name(
321 $table_name, $name || ++$idx_name_default
da8e499e 322 );
50840472 323 $name = next_unused_name($name, \%used_index_names);
324 $used_index_names{$name} = $name;
0c43e0a1 325 push @index_defs,
50840472 326 qq[CREATE INDEX "$name" on $table_name_ur ("].
327 join( '", "', @fields ).
328 '");';
da8e499e 329 }
f8f0253c 330 else {
0c43e0a1 331 warn "Unknown index type ($type) on table $table_name.\n"
da8e499e 332 if $WARN;
f8f0253c 333 }
334 }
335
0c43e0a1 336 #
337 # Table constraints
338 #
339 my $c_name_default;
340 for my $c ( $table->get_constraints ) {
341 my $name = $c->name || '';
342 my @fields =
343 map { $_ =~ s/\(.+\)//; $_ }
344 map { unreserve( $_, $table_name ) }
345 $c->fields;
346 my @rfields =
347 map { $_ =~ s/\(.+\)//; $_ }
348 map { unreserve( $_, $table_name ) }
349 $c->reference_fields;
8065e024 350 next if !@fields && $c->type ne CHECK_C;
0c43e0a1 351
352 if ( $c->type eq PRIMARY_KEY ) {
353 $name ||= mk_name( $table_name, 'pk' );
50840472 354 $name = next_unused_name($name, \%used_index_names);
355 $used_index_names{$name} = $name;
0c43e0a1 356 push @constraint_defs, "CONSTRAINT $name PRIMARY KEY ".
50840472 357 '("' . join( '", "', @fields ) . '")';
0c43e0a1 358 }
359 elsif ( $c->type eq UNIQUE ) {
360 $name ||= mk_name(
361 $table_name, $name || ++$c_name_default
362 );
50840472 363 $name = next_unused_name($name, \%used_index_names);
364 $used_index_names{$name} = $name;
0c43e0a1 365 push @constraint_defs, "CONSTRAINT $name UNIQUE " .
50840472 366 '("' . join( '", "', @fields ) . '")';
0c43e0a1 367 }
8065e024 368 elsif ( $c->type eq CHECK_C ) {
369 my $s;
370 if ( $name ) {
371 $name ||= mk_name(
372 $table_name, $name || ++$c_name_default
373 );
374 $name = next_unused_name($name, \%used_index_names);
375 $used_index_names{$name} = $name;
376 $s = 'CONSTRAINT $name ';
377 }
378 my $expression = $c->expression;
379 push @constraint_defs, "${s}CHECK ($expression)";
380 }
0c43e0a1 381 elsif ( $c->type eq FOREIGN_KEY ) {
382 my $def = join(' ',
383 map { $_ || () } 'FOREIGN KEY', $c->name
384 );
385
50840472 386 $def .= ' ("' . join( '", "', @fields ) . '")';
0c43e0a1 387
388 $def .= ' REFERENCES ' . $c->reference_table;
389
390 if ( @rfields ) {
50840472 391 $def .= ' ("' . join( '", "', @rfields ) . '")';
0c43e0a1 392 }
393
394 if ( $c->match_type ) {
395 $def .= ' MATCH ' .
396 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
397 }
398
399 if ( $c->on_delete ) {
400 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
401 }
402
403 if ( $c->on_update ) {
404 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
405 }
406
407 push @constraint_defs, $def;
408 }
409 }
410
da8e499e 411 my $create_statement;
412 $create_statement = qq[DROP TABLE "$table_name_ur";\n]
413 if $add_drop_table;
414 $create_statement .= qq[CREATE TABLE "$table_name_ur" (\n].
0c43e0a1 415 join( ",\n", map { " $_" } @field_defs, @constraint_defs ).
da8e499e 416 "\n);"
417 ;
418
419 $output .= join( "\n\n",
420 @comments,
0c43e0a1 421 @sequence_defs,
da8e499e 422 $create_statement,
0c43e0a1 423 @index_defs,
da8e499e 424 ''
425 );
426 }
427
428 if ( $WARN ) {
429 if ( %truncated ) {
430 warn "Truncated " . keys( %truncated ) . " names:\n";
431 warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
432 }
433
434 if ( %unreserve ) {
435 warn "Encounted " . keys( %unreserve ) .
436 " unsafe names in schema (reserved or invalid):\n";
437 warn "\t" . join( "\n\t", sort keys %unreserve ) . "\n";
438 }
f8f0253c 439 }
440
da8e499e 441 return $output;
f8f0253c 442}
443
96844cae 444# -------------------------------------------------------------------
445sub mk_name {
0c43e0a1 446 my $basename = shift || '';
447 my $type = shift || '';
448 my $scope = shift || '';
449 my $critical = shift || '';
96844cae 450 my $basename_orig = $basename;
2ad4c2c8 451 my $max_name = $type
452 ? $max_id_length - (length($type) + 1)
453 : $max_id_length;
96844cae 454 $basename = substr( $basename, 0, $max_name )
455 if length( $basename ) > $max_name;
456 my $name = $type ? "${type}_$basename" : $basename;
457
458 if ( $basename ne $basename_orig and $critical ) {
459 my $show_type = $type ? "+'$type'" : "";
460 warn "Truncating '$basename_orig'$show_type to $max_id_length ",
461 "character limit to make '$name'\n" if $WARN;
462 $truncated{ $basename_orig } = $name;
463 }
464
465 $scope ||= \%global_names;
466 if ( my $prev = $scope->{ $name } ) {
467 my $name_orig = $name;
468 $name .= sprintf( "%02d", ++$prev );
469 substr($name, $max_id_length - 3) = "00"
470 if length( $name ) > $max_id_length;
471
472 warn "The name '$name_orig' has been changed to ",
473 "'$name' to make it unique.\n" if $WARN;
474
475 $scope->{ $name_orig }++;
f8f0253c 476 }
96844cae 477
478 $scope->{ $name }++;
479 return $name;
480}
481
482# -------------------------------------------------------------------
483sub unreserve {
0c43e0a1 484 my $name = shift || '';
485 my $schema_obj_name = shift || '';
486
96844cae 487 my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
488
489 # also trap fields that don't begin with a letter
0c43e0a1 490 return $name if !$reserved{ uc $name } && $name =~ /^[a-z]/i;
96844cae 491
492 if ( $schema_obj_name ) {
493 ++$unreserve{"$schema_obj_name.$name"};
494 }
495 else {
496 ++$unreserve{"$name (table name)"};
497 }
498
499 my $unreserve = sprintf '%s_', $name;
500 return $unreserve.$suffix;
f8f0253c 501}
502
50840472 503# -------------------------------------------------------------------
504sub next_unused_name {
505 my $name = shift || '';
506 my $used_names = shift || '';
507
508 my %used_names = %$used_names;
509
510 if ( !defined($used_names{$name}) ) {
511 $used_names{$name} = $name;
512 return $name;
513 }
514
515 my $i = 2;
516 while ( defined($used_names{$name . $i}) ) {
517 ++$i;
518 }
519 $name .= $i;
520 $used_names{$name} = $name;
521 return $name;
522}
523
f8f0253c 5241;
f8f0253c 525
96844cae 526# -------------------------------------------------------------------
527# Life is full of misery, loneliness, and suffering --
528# and it's all over much too soon.
529# Woody Allen
530# -------------------------------------------------------------------
f8f0253c 531
96844cae 532=pod
f8f0253c 533
534=head1 AUTHOR
535
536Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
537
538=cut