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