Clean up properly after Parser::DBI::PostgreSQL tests
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / MySQL.pm
CommitLineData
9398955f 1package SQL::Translator::Producer::MySQL;
2
c855a748 3=head1 NAME
4
5SQL::Translator::Producer::MySQL - MySQL-specific producer for SQL::Translator
6
7=head1 SYNOPSIS
8
9Use via SQL::Translator:
10
11 use SQL::Translator;
12
13 my $t = SQL::Translator->new( parser => '...', producer => 'MySQL', '...' );
14 $t->translate;
15
16=head1 DESCRIPTION
17
18This module will produce text output of the schema suitable for MySQL.
ea93df61 19There are still some issues to be worked out with syntax differences
c855a748 20between MySQL versions 3 and 4 ("SET foreign_key_checks," character sets
21for fields, etc.).
22
ea93df61 23=head1 ARGUMENTS
5d666b31 24
ea93df61 25This producer takes a single optional producer_arg C<mysql_version>, which
5d666b31 26provides the desired version for the target database. By default MySQL v3 is
27assumed, and statements pertaining to any features introduced in later versions
28(e.g. CREATE VIEW) are not produced.
29
ea93df61 30Valid version specifiers for C<mysql_version> are listed L<here|SQL::Translator::Utils/parse_mysql_version>
5d666b31 31
5a0c7b43 32=head2 Table Types
33
34Normally the tables will be created without any explicit table type given and
35so will use the MySQL default.
36
37Any tables involved in foreign key constraints automatically get a table type
38of InnoDB, unless this is overridden by setting the C<mysql_table_type> extra
39attribute explicitly on the table.
40
41=head2 Extra attributes.
42
43The producer recognises the following extra attributes on the Schema objects.
44
45=over 4
46
7467c458 47=item B<field.list>
5a0c7b43 48
49Set the list of allowed values for Enum fields.
50
7467c458 51=item B<field.binary>, B<field.unsigned>, B<field.zerofill>
5a0c7b43 52
53Set the MySQL field options of the same name.
54
7467c458 55=item B<field.renamed_from>, B<table.renamed_from>
4d438549 56
7467c458 57Use when producing diffs to indicate that the current table/field has been
58renamed from the old name as given in the attribute value.
4d438549 59
7467c458 60=item B<table.mysql_table_type>
5a0c7b43 61
62Set the type of the table e.g. 'InnoDB', 'MyISAM'. This will be
63automatically set for tables involved in foreign key constraints if it is
64not already set explicitly. See L<"Table Types">.
65
10f70490 66Please note that the C<ENGINE> option is the preferred method of specifying
7467c458 67the MySQL storage engine to use, but this method still works for backwards
10f70490 68compatibility.
7467c458 69
70=item B<table.mysql_charset>, B<table.mysql_collate>
1ded8513 71
72Set the tables default charater set and collation order.
73
7467c458 74=item B<field.mysql_charset>, B<field.mysql_collate>
1ded8513 75
76Set the fields charater set and collation order.
77
5a0c7b43 78=back
79
c855a748 80=cut
81
9398955f 82use strict;
cd0ea0fd 83use warnings;
0c04c5a2 84our ( $DEBUG, %used_names );
85our $VERSION = '1.59';
5636ed00 86$DEBUG = 0 unless defined $DEBUG;
9398955f 87
f5405d47 88# Maximum length for most identifiers is 64, according to:
89# http://dev.mysql.com/doc/refman/4.1/en/identifiers.html
90# http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
91my $DEFAULT_MAX_ID_LENGTH = 64;
92
9398955f 93use Data::Dumper;
1c14e9f1 94use SQL::Translator::Schema::Constants;
ea93df61 95use SQL::Translator::Utils qw(debug header_comment
edc4b5da 96 truncate_id_uniquely parse_mysql_version);
9398955f 97
d2344c83 98#
99# Use only lowercase for the keys (e.g. "long" and not "LONG")
100#
2620fc1c 101my %translate = (
102 #
103 # Oracle types
104 #
105 varchar2 => 'varchar',
106 long => 'text',
d2344c83 107 clob => 'longtext',
2620fc1c 108
109 #
110 # Sybase types
111 #
112 int => 'integer',
113 money => 'float',
114 real => 'double',
115 comment => 'text',
116 bit => 'tinyint',
87779799 117
118 #
119 # Access types
120 #
121 'long integer' => 'integer',
122 'text' => 'text',
123 'datetime' => 'datetime',
08dd6593 124
125 #
126 # PostgreSQL types
127 #
128 bytea => 'BLOB',
2620fc1c 129);
130
de176728 131#
132# Column types that do not support lenth attribute
133#
134my @no_length_attr = qw/
135 date time timestamp datetime year
136 /;
137
9ab59f87 138
139sub preprocess_schema {
934e1b39 140 my ($schema) = @_;
9ab59f87 141
142 # extra->{mysql_table_type} used to be the type. It belongs in options, so
143 # move it if we find it. Return Engine type if found in extra or options
7725e1e6 144 # Similarly for mysql_charset and mysql_collate
145 my $extra_to_options = sub {
146 my ($table, $extra_name, $opt_name) = @_;
9ab59f87 147
148 my $extra = $table->extra;
149
7725e1e6 150 my $extra_type = delete $extra->{$extra_name};
9ab59f87 151
152 # Now just to find if there is already an Engine or Type option...
153 # and lets normalize it to ENGINE since:
154 #
ea93df61 155 # The ENGINE table option specifies the storage engine for the table.
9ab59f87 156 # TYPE is a synonym, but ENGINE is the preferred option name.
157 #
158
ea93df61 159 # We have to use the hash directly here since otherwise there is no way
9ab59f87 160 # to remove options.
161 my $options = ( $table->{options} ||= []);
162
7725e1e6 163 # If multiple option names, normalize to the first one
164 if (ref $opt_name) {
165 OPT_NAME: for ( @$opt_name[1..$#$opt_name] ) {
166 for my $idx ( 0..$#{$options} ) {
167 my ($key, $value) = %{ $options->[$idx] };
ea93df61 168
7725e1e6 169 if (uc $key eq $_) {
170 $options->[$idx] = { $opt_name->[0] => $value };
171 last OPT_NAME;
172 }
173 }
174 }
175 $opt_name = $opt_name->[0];
176
177 }
178
179
9ab59f87 180 # This assumes that there isn't both a Type and an Engine option.
7725e1e6 181 OPTION:
9ab59f87 182 for my $idx ( 0..$#{$options} ) {
183 my ($key, $value) = %{ $options->[$idx] };
184
7725e1e6 185 next unless uc $key eq $opt_name;
ea93df61 186
7725e1e6 187 # make sure case is right on option name
9ab59f87 188 delete $options->[$idx]{$key};
7725e1e6 189 return $options->[$idx]{$opt_name} = $value || $extra_type;
9ab59f87 190
191 }
ea93df61 192
9ab59f87 193 if ($extra_type) {
7725e1e6 194 push @$options, { $opt_name => $extra_type };
9ab59f87 195 return $extra_type;
196 }
197
198 };
199
1c680eb9 200 # Names are only specific to a given schema
201 local %used_names = ();
202
9ab59f87 203 #
204 # Work out which tables need to be InnoDB to support foreign key
205 # constraints. We do this first as we need InnoDB at both ends.
206 #
207 foreach my $table ( $schema->get_tables ) {
ea93df61 208
7725e1e6 209 $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE'] );
210 $extra_to_options->($table, 'mysql_charset', 'CHARACTER SET' );
211 $extra_to_options->($table, 'mysql_collate', 'COLLATE' );
9ab59f87 212
929ef265 213 foreach my $c ( $table->get_constraints ) {
1c680eb9 214 next unless $c->type eq FOREIGN_KEY;
215
216 # Normalize constraint names here.
217 my $c_name = $c->name;
218 # Give the constraint a name if it doesn't have one, so it doens't feel
219 # left out
220 $c_name = $table->name . '_fk' unless length $c_name;
ea93df61 221
1c680eb9 222 $c->name( next_unused_name($c_name) );
223
9ab59f87 224 for my $meth (qw/table reference_table/) {
1c680eb9 225 my $table = $schema->get_table($c->$meth) || next;
7725e1e6 226 # This normalizes the types to ENGINE and returns the value if its there
227 next if $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE']);
9ab59f87 228 $table->options( { 'ENGINE' => 'InnoDB' } );
229 }
929ef265 230 } # foreach constraints
231
2c6be67a 232 my %map = ( mysql_collate => 'collate', mysql_charset => 'character set');
929ef265 233 foreach my $f ( $table->get_fields ) {
2c6be67a 234 my $extra = $f->extra;
235 for (keys %map) {
236 $extra->{$map{$_}} = delete $extra->{$_} if exists $extra->{$_};
237 }
238
929ef265 239 my @size = $f->size;
240 if ( !$size[0] && $f->data_type =~ /char$/ ) {
241 $f->size( (255) );
242 }
9ab59f87 243 }
929ef265 244
9ab59f87 245 }
246}
247
9398955f 248sub produce {
a1d94525 249 my $translator = shift;
250 local $DEBUG = $translator->debug;
da5a1bae 251 local %used_names;
a1d94525 252 my $no_comments = $translator->no_comments;
253 my $add_drop_table = $translator->add_drop_table;
254 my $schema = $translator->schema;
2bc23e82 255 my $show_warnings = $translator->show_warnings || 0;
ca1f9923 256 my $producer_args = $translator->producer_args;
5d666b31 257 my $mysql_version = parse_mysql_version ($producer_args->{mysql_version}, 'perl') || 0;
f5405d47 258 my $max_id_length = $producer_args->{mysql_max_id_length} || $DEFAULT_MAX_ID_LENGTH;
d529894e 259
7725e1e6 260 my ($qt, $qf, $qc) = ('','', '');
fe0f47d0 261 $qt = '`' if $translator->quote_table_names;
262 $qf = '`' if $translator->quote_field_names;
263
1a24938d 264 debug("PKG: Beginning production\n");
da5a1bae 265 %used_names = ();
ea93df61 266 my $create = '';
5ee19df8 267 $create .= header_comment unless ($no_comments);
0823773d 268 # \todo Don't set if MySQL 3.x is set on command line
24d9fe69 269 my @create = "SET foreign_key_checks=0";
9398955f 270
934e1b39 271 preprocess_schema($schema);
5a0c7b43 272
273 #
274 # Generate sql
275 #
cd0ea0fd 276 my @table_defs =();
ea93df61 277
1c14e9f1 278 for my $table ( $schema->get_tables ) {
cd0ea0fd 279# print $table->name, "\n";
ea93df61 280 push @table_defs, create_table($table,
fe0f47d0 281 { add_drop_table => $add_drop_table,
282 show_warnings => $show_warnings,
283 no_comments => $no_comments,
284 quote_table_names => $qt,
ca1f9923 285 quote_field_names => $qf,
f5405d47 286 max_id_length => $max_id_length,
ca1f9923 287 mysql_version => $mysql_version
0013ee25 288 });
289 }
9398955f 290
e30b71b8 291 if ($mysql_version >= 5.000001) {
83ddfeac 292 for my $view ( $schema->get_views ) {
d28afa66 293 push @table_defs, create_view($view,
294 { add_replace_view => $add_drop_table,
295 show_warnings => $show_warnings,
296 no_comments => $no_comments,
297 quote_table_names => $qt,
298 quote_field_names => $qf,
299 max_id_length => $max_id_length,
300 mysql_version => $mysql_version
301 });
83ddfeac 302 }
d28afa66 303 }
304
ee22632e 305 if ($mysql_version >= 5.000002) {
306 for my $trigger ( $schema->get_triggers ) {
307 push @table_defs, create_trigger($trigger,
308 { add_drop_trigger => $add_drop_table,
309 show_warnings => $show_warnings,
310 no_comments => $no_comments,
311 quote_table_names => $qt,
312 quote_field_names => $qf,
313 max_id_length => $max_id_length,
314 mysql_version => $mysql_version
315 });
316 }
317 }
318
d28afa66 319
cd0ea0fd 320# print "@table_defs\n";
24d9fe69 321 push @table_defs, "SET foreign_key_checks=1";
cd0ea0fd 322
24d9fe69 323 return wantarray ? ($create ? $create : (), @create, @table_defs) : ($create . join('', map { $_ ? "$_;\n\n" : () } (@create, @table_defs)));
0013ee25 324}
9398955f 325
ee22632e 326sub create_trigger {
327 my ($trigger, $options) = @_;
328 my $qt = $options->{quote_table_names} || '';
329 my $qf = $options->{quote_field_names} || '';
330
331 my $trigger_name = $trigger->name;
332 debug("PKG: Looking at trigger '${trigger_name}'\n");
333
334 my @statements;
335
336 my $events = $trigger->database_events;
337 for my $event ( @$events ) {
338 my $name = $trigger_name;
339 if (@$events > 1) {
340 $name .= "_$event";
341
342 warn "Multiple database events supplied for trigger '${trigger_name}', ",
343 "creating trigger '${name}' for the '${event}' event\n"
344 if $options->{show_warnings};
345 }
346
347 my $action = $trigger->action;
348 $action .= ";" unless $action =~ /;\s*\z/;
349
350 push @statements, "DROP TRIGGER IF EXISTS ${qt}${name}${qt}" if $options->{add_drop_trigger};
351 push @statements, sprintf(
352 "CREATE TRIGGER ${qt}%s${qt} %s %s ON ${qt}%s${qt}\n FOR EACH ROW BEGIN %s END",
353 $name, $trigger->perform_action_when, $event, $trigger->on_table, $action,
354 );
355
356 }
357 # Tack the comment onto the first statement
358 $statements[0] = "--\n-- Trigger ${qt}${trigger_name}${qt}\n--\n" . $statements[0] unless $options->{no_comments};
359 return @statements;
360}
361
d28afa66 362sub create_view {
363 my ($view, $options) = @_;
364 my $qt = $options->{quote_table_names} || '';
365 my $qf = $options->{quote_field_names} || '';
366
367 my $view_name = $view->name;
368 debug("PKG: Looking at view '${view_name}'\n");
369
370 # Header. Should this look like what mysqldump produces?
371 my $create = '';
372 $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n" unless $options->{no_comments};
373 $create .= 'CREATE';
374 $create .= ' OR REPLACE' if $options->{add_replace_view};
375 $create .= "\n";
376
377 my $extra = $view->extra;
378 # ALGORITHM
379 if( exists($extra->{mysql_algorithm}) && defined(my $algorithm = $extra->{mysql_algorithm}) ){
380 $create .= " ALGORITHM = ${algorithm}\n" if $algorithm =~ /(?:UNDEFINED|MERGE|TEMPTABLE)/i;
381 }
382 # DEFINER
383 if( exists($extra->{mysql_definer}) && defined(my $user = $extra->{mysql_definer}) ){
384 $create .= " DEFINER = ${user}\n";
385 }
386 # SECURITY
387 if( exists($extra->{mysql_security}) && defined(my $security = $extra->{mysql_security}) ){
388 $create .= " SQL SECURITY ${security}\n" if $security =~ /(?:DEFINER|INVOKER)/i;
389 }
390
391 #Header, cont.
392 $create .= " VIEW ${qt}${view_name}${qt}";
393
394 if( my @fields = $view->fields ){
395 my $list = join ', ', map { "${qf}${_}${qf}"} @fields;
396 $create .= " ( ${list} )";
397 }
398 if( my $sql = $view->sql ){
1c8ec56e 399 # do not wrap parenthesis around the selector, mysql doesn't like this
400 # http://bugs.mysql.com/bug.php?id=9198
401 $create .= " AS\n ${sql}\n";
d28afa66 402 }
24d9fe69 403# $create .= "";
d28afa66 404 return $create;
405}
406
0013ee25 407sub create_table
408{
409 my ($table, $options) = @_;
2620fc1c 410
fe0f47d0 411 my $qt = $options->{quote_table_names} || '';
412 my $qf = $options->{quote_field_names} || '';
413
8d693a85 414 my $table_name = quote_table_name($table->name, $qt);
0013ee25 415 debug("PKG: Looking at table '$table_name'\n");
6d3f6379 416
0013ee25 417 #
418 # Header. Should this look like what mysqldump produces?
419 #
cd0ea0fd 420 my $create = '';
fa94b25f 421 my $drop;
8d693a85 422 $create .= "--\n-- Table: $table_name\n--\n" unless $options->{no_comments};
423 $drop = qq[DROP TABLE IF EXISTS $table_name] if $options->{add_drop_table};
424 $create .= "CREATE TABLE $table_name (\n";
2b695517 425
0013ee25 426 #
427 # Fields
428 #
429 my @field_defs;
430 for my $field ( $table->get_fields ) {
fe0f47d0 431 push @field_defs, create_field($field, $options);
0013ee25 432 }
1ded8513 433
0013ee25 434 #
435 # Indices
436 #
437 my @index_defs;
438 my %indexed_fields;
439 for my $index ( $table->get_indices ) {
fe0f47d0 440 push @index_defs, create_index($index, $options);
0013ee25 441 $indexed_fields{ $_ } = 1 for $index->fields;
442 }
d529894e 443
0013ee25 444 #
445 # Constraints -- need to handle more than just FK. -ky
446 #
447 my @constraint_defs;
448 my @constraints = $table->get_constraints;
449 for my $c ( @constraints ) {
cd0ea0fd 450 my $constr = create_constraint($c, $options);
451 push @constraint_defs, $constr if($constr);
ea93df61 452
da5a1bae 453 unless ( $indexed_fields{ ($c->fields())[0] } || $c->type ne FOREIGN_KEY ) {
454 push @index_defs, "INDEX ($qf" . ($c->fields())[0] . "$qf)";
455 $indexed_fields{ ($c->fields())[0] } = 1;
456 }
0013ee25 457 }
1ded8513 458
ea93df61 459 $create .= join(",\n", map { " $_" }
0013ee25 460 @field_defs, @index_defs, @constraint_defs
461 );
9398955f 462
0013ee25 463 #
464 # Footer
465 #
466 $create .= "\n)";
9a96648f 467 $create .= generate_table_options($table, $options) || '';
24d9fe69 468# $create .= ";\n\n";
9398955f 469
fa94b25f 470 return $drop ? ($drop,$create) : $create;
0013ee25 471}
f8b6e804 472
8d693a85 473sub quote_table_name {
474 my ($table_name, $qt) = @_;
475
476 $table_name =~ s/\./$qt.$qt/g;
477
478 return "$qt$table_name$qt";
479}
480
ea93df61 481sub generate_table_options
da5a1bae 482{
9a96648f 483 my ($table, $options) = @_;
da5a1bae 484 my $create;
485
486 my $table_type_defined = 0;
9a96648f 487 my $qf = $options->{quote_field_names} ||= '';
7725e1e6 488 my $charset = $table->extra('mysql_charset');
489 my $collate = $table->extra('mysql_collate');
9a96648f 490 my $union = undef;
da5a1bae 491 for my $t1_option_ref ( $table->options ) {
492 my($key, $value) = %{$t1_option_ref};
493 $table_type_defined = 1
494 if uc $key eq 'ENGINE' or uc $key eq 'TYPE';
7725e1e6 495 if (uc $key eq 'CHARACTER SET') {
496 $charset = $value;
497 next;
498 } elsif (uc $key eq 'COLLATE') {
499 $collate = $value;
500 next;
9a96648f 501 } elsif (uc $key eq 'UNION') {
502 $union = "($qf". join("$qf, $qf", @$value) ."$qf)";
503 next;
7725e1e6 504 }
da5a1bae 505 $create .= " $key=$value";
506 }
9ab59f87 507
da5a1bae 508 my $mysql_table_type = $table->extra('mysql_table_type');
9ab59f87 509 $create .= " ENGINE=$mysql_table_type"
da5a1bae 510 if $mysql_table_type && !$table_type_defined;
da5a1bae 511 my $comments = $table->comments;
512
513 $create .= " DEFAULT CHARACTER SET $charset" if $charset;
514 $create .= " COLLATE $collate" if $collate;
9a96648f 515 $create .= " UNION=$union" if $union;
da5a1bae 516 $create .= qq[ comment='$comments'] if $comments;
517 return $create;
518}
519
0013ee25 520sub create_field
521{
fe0f47d0 522 my ($field, $options) = @_;
523
524 my $qf = $options->{quote_field_names} ||= '';
9398955f 525
0013ee25 526 my $field_name = $field->name;
527 debug("PKG: Looking at field '$field_name'\n");
fe0f47d0 528 my $field_def = "$qf$field_name$qf";
0013ee25 529
530 # data type and size
531 my $data_type = $field->data_type;
532 my @size = $field->size;
533 my %extra = $field->extra;
534 my $list = $extra{'list'} || [];
535 # \todo deal with embedded quotes
536 my $commalist = join( ', ', map { qq['$_'] } @$list );
537 my $charset = $extra{'mysql_charset'};
538 my $collate = $extra{'mysql_collate'};
539
5d666b31 540 my $mysql_version = $options->{mysql_version} || 0;
0013ee25 541 #
542 # Oracle "number" type -- figure best MySQL type
543 #
544 if ( lc $data_type eq 'number' ) {
545 # not an integer
546 if ( scalar @size > 1 ) {
547 $data_type = 'double';
d529894e 548 }
0013ee25 549 elsif ( $size[0] && $size[0] >= 12 ) {
550 $data_type = 'bigint';
551 }
552 elsif ( $size[0] && $size[0] <= 1 ) {
553 $data_type = 'tinyint';
554 }
555 else {
556 $data_type = 'int';
557 }
558 }
559 #
560 # Convert a large Oracle varchar to "text"
5d666b31 561 # (not necessary as of 5.0.3 http://dev.mysql.com/doc/refman/5.0/en/char.html)
0013ee25 562 #
563 elsif ( $data_type =~ /char/i && $size[0] > 255 ) {
5d666b31 564 unless ($size[0] <= 65535 && $mysql_version >= 5.000003 ) {
565 $data_type = 'text';
566 @size = ();
567 }
0013ee25 568 }
0013ee25 569 elsif ( $data_type =~ /boolean/i ) {
ca1f9923 570 if ($mysql_version >= 4) {
571 $data_type = 'boolean';
572 } else {
573 $data_type = 'enum';
574 $commalist = "'0','1'";
575 }
0013ee25 576 }
577 elsif ( exists $translate{ lc $data_type } ) {
578 $data_type = $translate{ lc $data_type };
579 }
580
581 @size = () if $data_type =~ /(text|blob)/i;
582
583 if ( $data_type =~ /(double|float)/ && scalar @size == 1 ) {
584 push @size, '0';
585 }
d529894e 586
0013ee25 587 $field_def .= " $data_type";
588
7c1aae02 589 if ( lc($data_type) eq 'enum' || lc($data_type) eq 'set') {
0013ee25 590 $field_def .= '(' . $commalist . ')';
7c1aae02 591 }
ea93df61 592 elsif (
593 defined $size[0] && $size[0] > 0
594 &&
595 ! grep lc($data_type) eq $_, @no_length_attr
c857e37a 596 ) {
0013ee25 597 $field_def .= '(' . join( ', ', @size ) . ')';
598 }
599
600 # char sets
601 $field_def .= " CHARACTER SET $charset" if $charset;
602 $field_def .= " COLLATE $collate" if $collate;
603
604 # MySQL qualifiers
605 for my $qual ( qw[ binary unsigned zerofill ] ) {
606 my $val = $extra{ $qual } || $extra{ uc $qual } or next;
607 $field_def .= " $qual";
608 }
609 for my $qual ( 'character set', 'collate', 'on update' ) {
610 my $val = $extra{ $qual } || $extra{ uc $qual } or next;
611 $field_def .= " $qual $val";
612 }
613
614 # Null?
ad071409 615 if ( $field->is_nullable ) {
616 $field_def .= ' NULL';
617 }
618 else {
619 $field_def .= ' NOT NULL';
620 }
0013ee25 621
06baeb21 622 # Default?
623 SQL::Translator::Producer->_apply_default_value(
624 $field,
625 \$field_def,
626 [
627 'NULL' => \'NULL',
628 ],
629 );
0013ee25 630
631 if ( my $comments = $field->comments ) {
632 $field_def .= qq[ comment '$comments'];
633 }
634
635 # auto_increment?
636 $field_def .= " auto_increment" if $field->is_auto_increment;
637
638 return $field_def;
639}
640
da5a1bae 641sub alter_create_index
642{
643 my ($index, $options) = @_;
644
645 my $qt = $options->{quote_table_names} || '';
646 my $qf = $options->{quote_field_names} || '';
368e3eb7 647 my $table_name = quote_table_name($index->table->name, $qt);
da5a1bae 648 return join( ' ',
649 'ALTER TABLE',
368e3eb7 650 $table_name,
da5a1bae 651 'ADD',
652 create_index(@_)
653 );
654}
655
0013ee25 656sub create_index
657{
edc4b5da 658 my ( $index, $options ) = @_;
fe0f47d0 659
660 my $qf = $options->{quote_field_names} || '';
0013ee25 661
edc4b5da 662 return join(
663 ' ',
4ec6e692 664 map { $_ || () }
edc4b5da 665 lc $index->type eq 'normal' ? 'INDEX' : $index->type . ' INDEX',
666 $index->name
20476859 667 ? $qf . truncate_id_uniquely(
edc4b5da 668 $index->name,
669 $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH
20476859 670 ) . $qf
edc4b5da 671 : '',
672 '(' . $qf . join( "$qf, $qf", $index->fields ) . $qf . ')'
673 );
0013ee25 674}
675
da5a1bae 676sub alter_drop_index
677{
678 my ($index, $options) = @_;
679
680 my $qt = $options->{quote_table_names} || '';
681 my $qf = $options->{quote_field_names} || '';
368e3eb7 682 my $table_name = quote_table_name($index->table->name, $qt);
da5a1bae 683
ea93df61 684 return join( ' ',
da5a1bae 685 'ALTER TABLE',
368e3eb7 686 $table_name,
da5a1bae 687 'DROP',
688 'INDEX',
689 $index->name || $index->fields
690 );
691
692}
693
694sub alter_drop_constraint
695{
696 my ($c, $options) = @_;
697
698 my $qt = $options->{quote_table_names} || '';
7725e1e6 699 my $qc = $options->{quote_field_names} || '';
368e3eb7 700 my $table_name = quote_table_name($c->table->name, $qt);
da5a1bae 701
b62fa492 702 my @out = ('ALTER','TABLE',$table_name,'DROP');
703 if($c->type eq PRIMARY_KEY) {
704 push @out, $c->type;
705 }
706 else {
707 push @out, ($c->type eq FOREIGN_KEY ? $c->type : "INDEX"),
708 $qc . $c->name . $qc;
709 }
710 return join(' ',@out);
da5a1bae 711}
712
713sub alter_create_constraint
714{
715 my ($index, $options) = @_;
716
717 my $qt = $options->{quote_table_names} || '';
368e3eb7 718 my $table_name = quote_table_name($index->table->name, $qt);
da5a1bae 719 return join( ' ',
720 'ALTER TABLE',
368e3eb7 721 $table_name,
da5a1bae 722 'ADD',
723 create_constraint(@_) );
724}
725
0013ee25 726sub create_constraint
727{
728 my ($c, $options) = @_;
729
fb149f81 730 my $qf = $options->{quote_field_names} || '';
731 my $qt = $options->{quote_table_names} || '';
da5a1bae 732 my $leave_name = $options->{leave_name} || undef;
fe0f47d0 733
ee6e2ec3 734 my $reference_table_name = quote_table_name($c->reference_table, $qt);
735
0013ee25 736 my @fields = $c->fields or next;
737
738 if ( $c->type eq PRIMARY_KEY ) {
fe0f47d0 739 return 'PRIMARY KEY (' . $qf . join("$qf, $qf", @fields). $qf . ')';
0013ee25 740 }
741 elsif ( $c->type eq UNIQUE ) {
55da13f2 742 return sprintf 'UNIQUE %s(%s)',
743 ((defined $c->name && $c->name)
744 ? join ('',
745 $qf,
746 truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH ),
747 $qf,
748 ' '
749 )
750 : ''
751 ),
752 ( join ', ', map { "${qf}${_}${qf}" } @fields ),
753 ;
0013ee25 754 }
755 elsif ( $c->type eq FOREIGN_KEY ) {
d529894e 756 #
0013ee25 757 # Make sure FK field is indexed or MySQL complains.
5e56da9a 758 #
0013ee25 759
866d012e 760 my $table = $c->table;
f5405d47 761 my $c_name = truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH );
866d012e 762
ea93df61 763 my $def = join(' ',
764 map { $_ || () }
765 'CONSTRAINT',
766 $qf . $c_name . $qf,
fb149f81 767 'FOREIGN KEY'
768 );
0013ee25 769
da5a1bae 770
fe0f47d0 771 $def .= ' ('.$qf . join( "$qf, $qf", @fields ) . $qf . ')';
0013ee25 772
ee6e2ec3 773 $def .= ' REFERENCES ' . $reference_table_name;
0013ee25 774
775 my @rfields = map { $_ || () } $c->reference_fields;
776 unless ( @rfields ) {
777 my $rtable_name = $c->reference_table;
866d012e 778 if ( my $ref_table = $table->schema->get_table( $rtable_name ) ) {
0013ee25 779 push @rfields, $ref_table->primary_key;
1c14e9f1 780 }
0013ee25 781 else {
782 warn "Can't find reference table '$rtable_name' " .
783 "in schema\n" if $options->{show_warnings};
5e56da9a 784 }
785 }
786
0013ee25 787 if ( @rfields ) {
fe0f47d0 788 $def .= ' (' . $qf . join( "$qf, $qf", @rfields ) . $qf . ')';
0013ee25 789 }
790 else {
866d012e 791 warn "FK constraint on " . $table->name . '.' .
ea93df61 792 join('', @fields) . " has no reference fields\n"
0013ee25 793 if $options->{show_warnings};
794 }
5e56da9a 795
0013ee25 796 if ( $c->match_type ) {
ea93df61 797 $def .= ' MATCH ' .
0013ee25 798 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
799 }
800
801 if ( $c->on_delete ) {
5863ad87 802 $def .= ' ON DELETE '. $c->on_delete;
0013ee25 803 }
804
805 if ( $c->on_update ) {
5863ad87 806 $def .= ' ON UPDATE '. $c->on_update;
0013ee25 807 }
cd0ea0fd 808 return $def;
9398955f 809 }
810
cd0ea0fd 811 return undef;
0013ee25 812}
813
da5a1bae 814sub alter_table
815{
816 my ($to_table, $options) = @_;
817
7467c458 818 my $qt = $options->{quote_table_names} || '';
da5a1bae 819
9a96648f 820 my $table_options = generate_table_options($to_table, $options) || '';
368e3eb7 821 my $table_name = quote_table_name($to_table->name, $qt);
da5a1bae 822 my $out = sprintf('ALTER TABLE %s%s',
368e3eb7 823 $table_name,
da5a1bae 824 $table_options);
825
826 return $out;
827}
828
4d438549 829sub rename_field { alter_field(@_) }
0013ee25 830sub alter_field
831{
fe0f47d0 832 my ($from_field, $to_field, $options) = @_;
833
7467c458 834 my $qf = $options->{quote_field_names} || '';
835 my $qt = $options->{quote_table_names} || '';
368e3eb7 836 my $table_name = quote_table_name($to_field->table->name, $qt);
0013ee25 837
838 my $out = sprintf('ALTER TABLE %s CHANGE COLUMN %s %s',
368e3eb7 839 $table_name,
4d438549 840 $qf . $from_field->name . $qf,
fe0f47d0 841 create_field($to_field, $options));
0013ee25 842
843 return $out;
844}
845
846sub add_field
847{
fe0f47d0 848 my ($new_field, $options) = @_;
849
7467c458 850 my $qt = $options->{quote_table_names} || '';
368e3eb7 851 my $table_name = quote_table_name($new_field->table->name, $qt);
0013ee25 852
853 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
368e3eb7 854 $table_name,
fe0f47d0 855 create_field($new_field, $options));
0013ee25 856
857 return $out;
858
859}
860
861sub drop_field
ea93df61 862{
fe0f47d0 863 my ($old_field, $options) = @_;
0013ee25 864
7467c458 865 my $qf = $options->{quote_field_names} || '';
866 my $qt = $options->{quote_table_names} || '';
368e3eb7 867 my $table_name = quote_table_name($old_field->table->name, $qt);
ea93df61 868
0013ee25 869 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
368e3eb7 870 $table_name,
fe0f47d0 871 $qf . $old_field->name . $qf);
0013ee25 872
873 return $out;
ea93df61 874
9398955f 875}
876
4d438549 877sub batch_alter_table {
878 my ($table, $diff_hash, $options) = @_;
879
ea93df61 880 # InnoDB has an issue with dropping and re-adding a FK constraint under the
f9ed5d54 881 # name in a single alter statment, see: http://bugs.mysql.com/bug.php?id=13741
882 #
883 # We have to work round this.
884
885 my %fks_to_alter;
886 my %fks_to_drop = map {
ea93df61 887 $_->type eq FOREIGN_KEY
888 ? ( $_->name => $_ )
f9ed5d54 889 : ( )
890 } @{$diff_hash->{alter_drop_constraint} };
891
892 my %fks_to_create = map {
893 if ( $_->type eq FOREIGN_KEY) {
894 $fks_to_alter{$_->name} = $fks_to_drop{$_->name} if $fks_to_drop{$_->name};
895 ( $_->name => $_ );
896 } else { ( ) }
897 } @{$diff_hash->{alter_create_constraint} };
898
24d9fe69 899 my @drop_stmt;
f9ed5d54 900 if (scalar keys %fks_to_alter) {
901 $diff_hash->{alter_drop_constraint} = [
902 grep { !$fks_to_alter{$_->name} } @{ $diff_hash->{alter_drop_constraint} }
903 ];
904
24d9fe69 905 @drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options);
f9ed5d54 906
907 }
908
4d438549 909 my @stmts = map {
910 if (@{ $diff_hash->{$_} || [] }) {
911 my $meth = __PACKAGE__->can($_) or die __PACKAGE__ . " cant $_";
f9ed5d54 912 map { $meth->( (ref $_ eq 'ARRAY' ? @$_ : $_), $options ) } @{ $diff_hash->{$_} }
4d438549 913 } else { () }
46bf5655 914 } qw/rename_table
915 alter_drop_constraint
4d438549 916 alter_drop_index
917 drop_field
918 add_field
919 alter_field
920 rename_field
921 alter_create_index
922 alter_create_constraint
923 alter_table/;
924
368e3eb7 925 #quote
926 my $qt = $options->{quote_table_names} || '';
927
46bf5655 928 # rename_table makes things a bit more complex
929 my $renamed_from = "";
368e3eb7 930 $renamed_from = quote_table_name($diff_hash->{rename_table}[0][0]->name, $qt)
46bf5655 931 if $diff_hash->{rename_table} && @{$diff_hash->{rename_table}};
932
4d438549 933 return unless @stmts;
934 # Just zero or one stmts. return now
24d9fe69 935 return (@drop_stmt,@stmts) unless @stmts > 1;
4d438549 936
937 # Now strip off the 'ALTER TABLE xyz' of all but the first one
938
368e3eb7 939 my $table_name = quote_table_name($table->name, $qt);
4104f82b 940
ea93df61 941 my $re = $renamed_from
368e3eb7 942 ? qr/^ALTER TABLE (?:\Q$table_name\E|\Q$renamed_from\E) /
4104f82b 943 : qr/^ALTER TABLE \Q$table_name\E /;
4d438549 944
945 my $first = shift @stmts;
4104f82b 946 my ($alter_table) = $first =~ /($re)/;
46bf5655 947
4d438549 948 my $padd = " " x length($alter_table);
949
24d9fe69 950 return @drop_stmt, join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts);
4104f82b 951
4d438549 952}
953
954sub drop_table {
46bf5655 955 my ($table, $options) = @_;
956
957 my $qt = $options->{quote_table_names} || '';
4d438549 958
959 # Drop (foreign key) constraints so table drops cleanly
46bf5655 960 my @sql = batch_alter_table($table, { alter_drop_constraint => [ grep { $_->type eq 'FOREIGN KEY' } $table->get_constraints ] }, $options);
961
368e3eb7 962 my $table_name = quote_table_name($table, $qt);
963 return (@sql, "DROP TABLE $table");
46bf5655 964
965}
966
967sub rename_table {
968 my ($old_table, $new_table, $options) = @_;
4d438549 969
46bf5655 970 my $qt = $options->{quote_table_names} || '';
368e3eb7 971 my $old_table_name = quote_table_name($old_table, $qt);
972 my $new_table_name = quote_table_name($new_table, $qt);
4d438549 973
368e3eb7 974 return "ALTER TABLE $old_table_name RENAME TO $new_table_name";
4d438549 975}
976
da5a1bae 977sub next_unused_name {
978 my $name = shift || '';
979 if ( !defined($used_names{$name}) ) {
980 $used_names{$name} = $name;
981 return $name;
982 }
983
984 my $i = 1;
985 while ( defined($used_names{$name . '_' . $i}) ) {
986 ++$i;
987 }
988 $name .= '_' . $i;
989 $used_names{$name} = $name;
990 return $name;
991}
992
9398955f 9931;
9398955f 994
c855a748 995=pod
996
997=head1 SEE ALSO
998
999SQL::Translator, http://www.mysql.com/.
9398955f 1000
2d6979da 1001=head1 AUTHORS
9398955f 1002
758ab1cd 1003darren chamberlain E<lt>darren@cpan.orgE<gt>,
f997b9ab 1004Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
c855a748 1005
1006=cut