Document delay_constraints producer_args option
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / Oracle.pm
1 package SQL::Translator::Producer::Oracle;
2
3 # -------------------------------------------------------------------
4 # $Id: Oracle.pm,v 1.34 2005-08-10 16:33:39 duality72 Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-4 SQLFairy Authors
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation; version 2.
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20 # 02111-1307  USA
21 # -------------------------------------------------------------------
22
23 =head1 NAME
24
25 SQL::Translator::Producer::Oracle - Oracle SQL producer
26
27 =head1 SYNOPSIS
28
29   use SQL::Translator;
30
31   my $t = SQL::Translator->new( parser => '...', producer => 'Oracle' );
32   print $translator->translate( $file );
33
34 =head1 DESCRIPTION
35
36 Creates an SQL DDL suitable for Oracle.
37
38 =head1 producer_args
39
40 =over
41
42 =item delay_constraints
43
44 This option remove the primary key and other key constraints from the
45 CREATE TABLE statement and adds ALTER TABLEs at the end with it.
46
47 =back
48
49 =cut
50
51 use strict;
52 use vars qw[ $VERSION $DEBUG $WARN ];
53 $VERSION = sprintf "%d.%02d", q$Revision: 1.34 $ =~ /(\d+)\.(\d+)/;
54 $DEBUG   = 0 unless defined $DEBUG;
55
56 use SQL::Translator::Schema::Constants;
57 use SQL::Translator::Utils qw(header_comment);
58
59 my %translate  = (
60     #
61     # MySQL types
62     #
63     bigint     => 'number',
64     double     => 'number',
65     decimal    => 'number',
66     float      => 'number',
67     int        => 'number',
68     integer    => 'number',
69     mediumint  => 'number',
70     smallint   => 'number',
71     tinyint    => 'number',
72     char       => 'char',
73     varchar    => 'varchar2',
74     tinyblob   => 'blob',
75     blob       => 'blob',
76     mediumblob => 'blob',
77     longblob   => 'blob',
78     tinytext   => 'varchar2',
79     text       => 'clob',
80     longtext   => 'clob',
81     mediumtext => 'clob',
82     enum       => 'varchar2',
83     set        => 'varchar2',
84     date       => 'date',
85     datetime   => 'date',
86     time       => 'date',
87     timestamp  => 'date',
88     year       => 'date',
89
90     #
91     # PostgreSQL types
92     #
93     numeric             => 'number',
94     'double precision'  => 'number',
95     serial              => 'number',
96     bigserial           => 'number',
97     money               => 'number',
98     character           => 'char',
99     'character varying' => 'varchar2',
100     bytea               => 'BLOB',
101     interval            => 'number',
102     boolean             => 'number',
103     point               => 'number',
104     line                => 'number',
105     lseg                => 'number',
106     box                 => 'number',
107     path                => 'number',
108     polygon             => 'number',
109     circle              => 'number',
110     cidr                => 'number',
111     inet                => 'varchar2',
112     macaddr             => 'varchar2',
113     bit                 => 'number',
114     'bit varying'       => 'number',
115
116     #
117     # Oracle types
118     #
119     number              => 'number',
120     varchar2            => 'varchar2',
121     long                => 'clob',
122 );
123
124 #
125 # Oracle reserved words from:
126 # http://technet.oracle.com/docs/products/oracle8i/doc_library/\
127 # 817_doc/server.817/a85397/ap_keywd.htm
128 #
129 my %ora_reserved = map { $_, 1 } qw(
130     ACCESS ADD ALL ALTER AND ANY AS ASC AUDIT 
131     BETWEEN BY
132     CHAR CHECK CLUSTER COLUMN COMMENT COMPRESS CONNECT CREATE CURRENT
133     DATE DECIMAL DEFAULT DELETE DESC DISTINCT DROP
134     ELSE EXCLUSIVE EXISTS 
135     FILE FLOAT FOR FROM
136     GRANT GROUP 
137     HAVING
138     IDENTIFIED IMMEDIATE IN INCREMENT INDEX INITIAL INSERT
139     INTEGER INTERSECT INTO IS
140     LEVEL LIKE LOCK LONG 
141     MAXEXTENTS MINUS MLSLABEL MODE MODIFY 
142     NOAUDIT NOCOMPRESS NOT NOWAIT NULL NUMBER 
143     OF OFFLINE ON ONLINE OPTION OR ORDER
144     PCTFREE PRIOR PRIVILEGES PUBLIC
145     RAW RENAME RESOURCE REVOKE ROW ROWID ROWNUM ROWS
146     SELECT SESSION SET SHARE SIZE SMALLINT START 
147     SUCCESSFUL SYNONYM SYSDATE 
148     TABLE THEN TO TRIGGER 
149     UID UNION UNIQUE UPDATE USER
150     VALIDATE VALUES VARCHAR VARCHAR2 VIEW
151     WHENEVER WHERE WITH
152 );
153
154 my $max_id_length    = 30;
155 my %used_identifiers = ();
156 my %global_names;
157 my %unreserve;
158 my %truncated;
159
160 # -------------------------------------------------------------------
161 sub produce {
162     my $translator     = shift;
163     $DEBUG             = $translator->debug;
164     $WARN              = $translator->show_warnings || 0;
165     my $no_comments    = $translator->no_comments;
166     my $add_drop_table = $translator->add_drop_table;
167     my $schema         = $translator->schema;
168     my $delay_constraints = $translator->producer_args->{delay_constraints};
169     my ($output, $create, @table_defs, @fk_defs, @trigger_defs, @index_defs, @constraint_defs);
170
171     $create .= header_comment unless ($no_comments);
172
173     if ( $translator->parser_type =~ /mysql/i ) {
174         $create .= 
175             "-- We assume that default NLS_DATE_FORMAT has been changed\n".
176             "-- but we set it here anyway to be self-consistent.\n"
177             unless $no_comments;
178
179         $create .= 
180         "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS';\n\n";
181     }
182
183     for my $table ( $schema->get_tables ) { 
184         my ( $table_def, $fk_def, $trigger_def, $index_def, $constraint_def) = create_table(
185             $table,
186             {
187                 add_drop_table => $add_drop_table,
188                 show_warnings  => $WARN,
189                 no_comments    => $no_comments,
190                 delay_constraints => $delay_constraints
191             }
192         );
193         push @table_defs, @$table_def;
194         push @fk_defs, @$fk_def;
195         push @trigger_defs, @$trigger_def;
196         push @index_defs, @$index_def;
197         push @constraint_defs, @$constraint_def;
198     }
199
200     my (@view_defs);
201     foreach my $view ( $schema->get_views ) {
202         push @view_defs, create_view($view);
203     }
204
205     return wantarray ? (defined $create ? $create : (), @table_defs, @view_defs, @fk_defs, @trigger_defs, @index_defs, @constraint_defs) : $create . join ("\n\n", @table_defs, @view_defs, @fk_defs, @trigger_defs, @index_defs, @constraint_defs, '');
206 }
207
208 sub create_table {
209     my ($table, $options) = @_;
210     my $table_name = $table->name;
211     
212     my $item = '';
213     my $drop;
214     my (@create, @field_defs, @constraint_defs, @fk_defs, @trigger_defs);
215
216     push @create, "--\n-- Table: $table_name\n--" unless $options->{no_comments};
217     push @create, qq[DROP TABLE $table_name CASCADE CONSTRAINTS;] if $options->{add_drop_table};
218
219     my $table_name_ur = unreserve($table_name) or next;
220
221         my ( %field_name_scope, @field_comments );
222         for my $field ( $table->get_fields ) {
223             #
224             # Field name
225             #
226             my $field_name    = mk_name(
227                 $field->name, '', \%field_name_scope, 1 
228             );
229             my $field_name_ur = unreserve( $field_name, $table_name );
230             my $field_def     = $field_name_ur;
231             $field->name( $field_name_ur );
232
233             #
234             # Datatype
235             #
236             my $check;
237             my $data_type = lc $field->data_type;
238             my @size      = $field->size;
239             my %extra     = $field->extra;
240             my $list      = $extra{'list'} || [];
241             # \todo deal with embedded quotes
242             my $commalist = join( ', ', map { qq['$_'] } @$list );
243
244             if ( $data_type eq 'enum' ) {
245                 $check = "CHECK ($field_name_ur IN ($commalist))";
246                 $data_type = 'varchar2';
247             }
248             elsif ( $data_type eq 'set' ) {
249                 # XXX add a CHECK constraint maybe 
250                 # (trickier and slower, than enum :)
251                 $data_type = 'varchar2';
252             }
253             else {
254                 $data_type  = defined $translate{ $data_type } ?
255                               $translate{ $data_type } :
256                               $data_type;
257                 $data_type ||= 'varchar2';
258             }
259             
260             #
261             # Fixes ORA-02329: column of datatype LOB cannot be 
262             # unique or a primary key
263             #
264             if ( $data_type eq 'clob' && $field->is_primary_key ) {
265                 $data_type = 'varchar2';
266                 $size[0]   = 4000;
267                 warn "CLOB cannot be a primary key, changing to VARCHAR2\n"
268                     if $WARN;
269             }
270
271             if ( $data_type eq 'clob' && $field->is_unique ) {
272                 $data_type = 'varchar2';
273                 $size[0]   = 4000;
274                 warn "CLOB cannot be a unique key, changing to VARCHAR2\n"
275                     if $WARN;
276             }
277
278             #
279             # Fixes ORA-00907: missing right parenthesis
280             #
281             if ( $data_type =~ /(date|clob)/i ) {
282                 undef @size;
283             }
284
285             $field_def .= " $data_type";
286             if ( defined $size[0] && $size[0] > 0 ) {
287                 $field_def .= '(' . join( ', ', @size ) . ')';
288             }
289
290             #
291             # Default value
292             #
293             my $default = $field->default_value;
294             if ( defined $default ) {
295                 #
296                 # Wherein we try to catch a string being used as 
297                 # a default value for a numerical field.  If "true/false,"
298                 # then sub "1/0," otherwise just test the truthity of the
299                 # argument and use that (naive?).
300                 #
301                 if ( 
302                     $data_type =~ /^number$/i && 
303                     $default   !~ /^-?\d+$/     &&
304                     $default   !~ m/null/i
305                 ) {
306                     if ( $default =~ /^true$/i ) {
307                         $default = "'1'";
308                     }
309                     elsif ( $default =~ /^false$/i ) {
310                         $default = "'0'";
311                     }
312                     else {
313                         $default = $default ? "'1'" : "'0'";
314                     }
315                 }
316                 elsif ( 
317                     $data_type =~ /date/ && (
318                         $default eq 'current_timestamp' 
319                         ||
320                         $default eq 'now()' 
321                     )
322                 ) {
323                     $default = 'SYSDATE';
324                 }
325                 else {
326                     $default = $default =~ m/null/i ? 'NULL' : "'$default'"
327                 } 
328
329                 $field_def .= " DEFAULT $default",
330             }
331
332             #
333             # Not null constraint
334             #
335             unless ( $field->is_nullable ) {
336                 $field_def .= ' NOT NULL';
337             }
338
339             $field_def .= " $check" if $check;
340
341             #
342             # Auto_increment
343             #
344             if ( $field->is_auto_increment ) {
345                 my $base_name    = $table_name_ur . "_". $field_name;
346                 my $seq_name     = mk_name( $base_name, 'sq' );
347                 my $trigger_name = mk_name( $base_name, 'ai' );
348
349             push @create, qq[DROP SEQUENCE $seq_name;] if $options->{add_drop_table};
350             push @create, "CREATE SEQUENCE $seq_name;";
351                 push @trigger_defs, 
352                     "CREATE OR REPLACE TRIGGER $trigger_name\n" .
353                     "BEFORE INSERT ON $table_name_ur\n" .
354                     "FOR EACH ROW WHEN (\n" .
355                         " new.$field_name_ur IS NULL".
356                         " OR new.$field_name_ur = 0\n".
357                     ")\n".
358                     "BEGIN\n" .
359                         " SELECT $seq_name.nextval\n" .
360                         " INTO :new." . $field->name."\n" .
361                         " FROM dual;\n" .
362                     "END;\n/";
363                 ;
364             }
365
366             if ( lc $field->data_type eq 'timestamp' ) {
367                 my $base_name = $table_name_ur . "_". $field_name_ur;
368                 my $trig_name = mk_name( $base_name, 'ts' );
369                 push @trigger_defs, 
370                     "CREATE OR REPLACE TRIGGER $trig_name\n".
371                     "BEFORE INSERT OR UPDATE ON $table_name_ur\n".
372                     "FOR EACH ROW WHEN (new.$field_name_ur IS NULL)\n".
373                     "BEGIN \n".
374                     " SELECT sysdate INTO :new.$field_name_ur FROM dual;\n".
375                     "END;\n/";
376             }
377
378             push @field_defs, $field_def;
379
380             if ( my $comment = $field->comments ) {
381                 $comment =~ s/'/''/g;
382                 push @field_comments, 
383                     "COMMENT ON COLUMN $table_name_ur.$field_name_ur is\n '" .
384                 $comment . "';" unless $options->{no_comments};
385             }
386         }
387
388         #
389         # Table options
390         #
391         my @table_options;
392         for my $opt ( $table->options ) {
393             if ( ref $opt eq 'HASH' ) {
394                 my ( $key, $value ) = each %$opt;
395                 if ( ref $value eq 'ARRAY' ) {
396                     push @table_options, "$key\n(\n".  join ("\n",
397                         map { "  $_->[0]\t$_->[1]" } 
398                         map { [ each %$_ ] }
399                         @$value
400                     )."\n)";
401                 }
402                 elsif ( !defined $value ) {
403                     push @table_options, $key;
404                 }
405                 else {
406                     push @table_options, "$key    $value";
407                 }
408             }
409         }
410
411         #
412         # Table constraints
413         #
414         for my $c ( $table->get_constraints ) {
415             my $name    = $c->name || '';
416             my @fields  = map { unreserve( $_, $table_name ) } $c->fields;
417             my @rfields = map { unreserve( $_, $table_name ) } 
418                 $c->reference_fields;
419             next if !@fields && $c->type ne CHECK_C;
420
421             if ( $c->type eq PRIMARY_KEY ) {
422                 #$name ||= mk_name( $table_name, 'pk' );
423                 push @constraint_defs, ($name ? "CONSTRAINT $name " : '') .
424                         'PRIMARY KEY (' . join( ', ', @fields ) . ')';
425             }
426             elsif ( $c->type eq UNIQUE ) {
427                 # Don't create UNIQUE constraints identical to the primary key
428                 if ( my $pk = $table->primary_key ) {
429                                         my $u_fields = join(":", @fields);
430                                         my $pk_fields = join(":", $pk->fields);
431                                         next if $u_fields eq $pk_fields;
432                 }
433
434                 $name ||= mk_name( $name || $table_name, 'u' );
435
436                 for my $f ( $c->fields ) {
437                     my $field_def = $table->get_field( $f ) or next;
438                     my $dtype     = $translate{ $field_def->data_type } or next;
439                     if ( $WARN && $dtype =~ /clob/i ) {
440                         warn "Oracle will not allow UNIQUE constraints on " .
441                              "CLOB field '" . $field_def->table->name . '.' .
442                              $field_def->name . ".'\n"
443                     }
444                 }
445
446                 push @constraint_defs, "CONSTRAINT $name UNIQUE " .
447                     '(' . join( ', ', @fields ) . ')';
448             }
449             elsif ( $c->type eq CHECK_C ) {
450                 $name ||= mk_name( $name || $table_name, 'ck' );
451                 my $expression = $c->expression || '';
452                 push @constraint_defs, "CONSTRAINT $name CHECK ($expression)";
453             }
454             elsif ( $c->type eq FOREIGN_KEY ) {
455             $name = mk_name( join('_', $table_name, $c->fields). '_fk' );
456                 my $def = "CONSTRAINT $name FOREIGN KEY ";
457
458                 if ( @fields ) {
459                     $def .= '(' . join( ', ', @fields ) . ')';
460                 }
461
462                 my $ref_table = unreserve($c->reference_table);
463
464                 $def .= " REFERENCES $ref_table";
465
466                 if ( @rfields ) {
467                     $def .= ' (' . join( ', ', @rfields ) . ')';
468                 }
469
470                 if ( $c->match_type ) {
471                     $def .= ' MATCH ' . 
472                         ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
473                 }
474
475                 if ( $c->on_delete ) {
476                     $def .= ' ON DELETE '.join( ' ', $c->on_delete );
477                 }
478
479             # disabled by plu 2007-12-29 - doesn't exist for oracle
480             #if ( $c->on_update ) {
481             #    $def .= ' ON UPDATE '.join( ' ', $c->on_update );
482             #}
483
484             push @fk_defs, sprintf("ALTER TABLE %s ADD %s;", $table, $def);
485             }
486         }
487
488         #
489         # Index Declarations
490         #
491         my @index_defs = ();
492         for my $index ( $table->get_indices ) {
493             my $index_name = $index->name || '';
494             my $index_type = $index->type || NORMAL;
495             my @fields     = map { unreserve( $_, $table_name ) }
496                              $index->fields;
497             next unless @fields;
498
499             my @index_options;
500             for my $opt ( $index->options ) {
501                 if ( ref $opt eq 'HASH' ) {
502                     my ( $key, $value ) = each %$opt;
503                     if ( ref $value eq 'ARRAY' ) {
504                         push @table_options, "$key\n(\n".  join ("\n",
505                             map { "  $_->[0]\t$_->[1]" } 
506                             map { [ each %$_ ] }
507                            @$value
508                         )."\n)";
509                     }
510                     elsif ( !defined $value ) {
511                         push @index_options, $key;
512                     }
513                     else {
514                         push @index_options, "$key    $value";
515                     }
516                 }
517             }
518             my $index_options = @index_options
519               ? "\n".join("\n", @index_options) : '';
520
521             if ( $index_type eq PRIMARY_KEY ) {
522                 $index_name = $index_name ? mk_name( $index_name ) 
523                     : mk_name( $table_name, 'pk' );
524                 push @field_defs, 'CONSTRAINT '.$index_name.' PRIMARY KEY '.
525                     '(' . join( ', ', @fields ) . ')';
526             }
527             elsif ( $index_type eq NORMAL ) {
528                 $index_name = $index_name ? mk_name( $index_name ) 
529                     : mk_name( $table_name, $index_name || 'i' );
530                 push @index_defs, 
531                     "CREATE INDEX $index_name on $table_name_ur (".
532                         join( ', ', @fields ).  
533                     ")$index_options;";
534             }
535             elsif ( $index_type eq UNIQUE ) {
536                 $index_name = $index_name ? mk_name( $index_name ) 
537                     : mk_name( $table_name, $index_name || 'i' );
538                 push @index_defs, 
539                     "CREATE UNIQUE INDEX $index_name on $table_name_ur (".
540                         join( ', ', @fields ).  
541                     ")$index_options;"; 
542             }
543             else {
544                 warn "Unknown index type ($index_type) on table $table_name.\n"
545                     if $WARN;
546             }
547         }
548
549         if ( my @table_comments = $table->comments ) {
550             for my $comment ( @table_comments ) {
551                 next unless $comment;
552                 $comment =~ s/'/''/g;
553                 push @field_comments, "COMMENT ON TABLE $table_name_ur is\n '".
554                 $comment . "';" unless $options->{no_comments}
555                 ;
556             }
557         }
558
559         my $table_options = @table_options 
560             ? "\n".join("\n", @table_options) : '';
561     push @create, "CREATE TABLE $table_name_ur (\n" .
562             join( ",\n", map { "  $_" } @field_defs,
563             ($options->{delay_constraints} ? () : @constraint_defs) ) .
564             "\n)$table_options;";
565
566     @constraint_defs = map { 'ALTER TABLE '.$table_name_ur.' ADD '.$_  }
567       @constraint_defs;
568
569     if ( $WARN ) {
570         if ( %truncated ) {
571             warn "Truncated " . keys( %truncated ) . " names:\n";
572             warn "\t" . join( "\n\t", sort keys %truncated ) . "\n";
573         }
574
575         if ( %unreserve ) {
576             warn "Encounted " . keys( %unreserve ) .
577                 " unsafe names in schema (reserved or invalid):\n";
578             warn "\t" . join( "\n\t", sort keys %unreserve ) . "\n";
579         }
580     }
581
582     return \@create, \@fk_defs, \@trigger_defs, \@index_defs, ($options->{delay_constraints} ? \@constraint_defs : []);
583 }
584
585 sub create_view {
586     my ($view) = @_;
587
588     my $out = sprintf("CREATE VIEW %s AS\n%s;",
589                       $view->name,
590                       $view->sql);
591
592     return $out;
593 }
594
595 # -------------------------------------------------------------------
596 sub mk_name {
597     my $basename      = shift || ''; 
598     my $type          = shift || ''; 
599        $type          = '' if $type =~ /^\d/;
600     my $scope         = shift || ''; 
601     my $critical      = shift || '';
602     my $basename_orig = $basename;
603     my $max_name      = $type 
604                         ? $max_id_length - (length($type) + 1) 
605                         : $max_id_length;
606     $basename         = substr( $basename, 0, $max_name ) 
607                         if length( $basename ) > $max_name;
608     my $name          = $type ? "${type}_$basename" : $basename;
609
610     if ( $basename ne $basename_orig and $critical ) {
611         my $show_type = $type ? "+'$type'" : "";
612         warn "Truncating '$basename_orig'$show_type to $max_id_length ",
613             "character limit to make '$name'\n" if $WARN;
614         $truncated{ $basename_orig } = $name;
615     }
616
617     $scope ||= \%global_names;
618     if ( my $prev = $scope->{ $name } ) {
619         my $name_orig = $name;
620         substr($name, $max_id_length - 2) = ""
621             if length( $name ) >= $max_id_length - 1;
622         $name        .= sprintf( "%02d", $prev++ );
623
624         warn "The name '$name_orig' has been changed to ",
625              "'$name' to make it unique.\n" if $WARN;
626
627         $scope->{ $name_orig }++;
628     }
629
630     $scope->{ $name }++;
631     return $name;
632 }
633
634 # -------------------------------------------------------------------
635 sub unreserve {
636     my $name            = shift || '';
637     my $schema_obj_name = shift || '';
638
639     my ( $suffix ) = ( $name =~ s/(\W.*)$// ) ? $1 : '';
640
641     # also trap fields that don't begin with a letter
642     return $name if !$ora_reserved{ uc $name } && $name =~ /^[a-z]/i; 
643
644     if ( $schema_obj_name ) {
645         ++$unreserve{"$schema_obj_name.$name"};
646     }
647     else {
648         ++$unreserve{"$name (table name)"};
649     }
650
651     my $unreserve = sprintf '%s_', $name;
652     return $unreserve.$suffix;
653 }
654
655 1;
656
657 # -------------------------------------------------------------------
658 # All bad art is the result of good intentions.
659 # Oscar Wilde
660 # -------------------------------------------------------------------
661
662 =pod
663
664 =head1 CREDITS
665
666 Mad props to Tim Bunce for much of the logic stolen from his "mysql2ora"
667 script.
668
669 =head1 AUTHOR
670
671 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
672
673 =head1 SEE ALSO
674
675 SQL::Translator, DDL::Oracle, mysql2ora.
676
677 =cut