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