Fix false-diffing due to order of table options
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / MySQL.pm
CommitLineData
9398955f 1package SQL::Translator::Producer::MySQL;
2
49e1eb70 3# -------------------------------------------------------------------
866d012e 4# $Id: MySQL.pm,v 1.54 2007-11-10 03:36:43 mwz444 Exp $
49e1eb70 5# -------------------------------------------------------------------
977651a5 6# Copyright (C) 2002-4 SQLFairy Authors
9398955f 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
c855a748 23=head1 NAME
24
25SQL::Translator::Producer::MySQL - MySQL-specific producer for SQL::Translator
26
27=head1 SYNOPSIS
28
29Use via SQL::Translator:
30
31 use SQL::Translator;
32
33 my $t = SQL::Translator->new( parser => '...', producer => 'MySQL', '...' );
34 $t->translate;
35
36=head1 DESCRIPTION
37
38This module will produce text output of the schema suitable for MySQL.
39There are still some issues to be worked out with syntax differences
40between MySQL versions 3 and 4 ("SET foreign_key_checks," character sets
41for fields, etc.).
42
5a0c7b43 43=head2 Table Types
44
45Normally the tables will be created without any explicit table type given and
46so will use the MySQL default.
47
48Any tables involved in foreign key constraints automatically get a table type
49of InnoDB, unless this is overridden by setting the C<mysql_table_type> extra
50attribute explicitly on the table.
51
52=head2 Extra attributes.
53
54The producer recognises the following extra attributes on the Schema objects.
55
56=over 4
57
7467c458 58=item B<field.list>
5a0c7b43 59
60Set the list of allowed values for Enum fields.
61
7467c458 62=item B<field.binary>, B<field.unsigned>, B<field.zerofill>
5a0c7b43 63
64Set the MySQL field options of the same name.
65
7467c458 66=item B<field.renamed_from>, B<table.renamed_from>
4d438549 67
7467c458 68Use when producing diffs to indicate that the current table/field has been
69renamed from the old name as given in the attribute value.
4d438549 70
7467c458 71=item B<table.mysql_table_type>
5a0c7b43 72
73Set the type of the table e.g. 'InnoDB', 'MyISAM'. This will be
74automatically set for tables involved in foreign key constraints if it is
75not already set explicitly. See L<"Table Types">.
76
7467c458 77Please note that the C<ENGINE> option is the prefered method of specifying
78the MySQL storage engine to use, but this method still works for backwards
79compatability.
80
81=item B<table.mysql_charset>, B<table.mysql_collate>
1ded8513 82
83Set the tables default charater set and collation order.
84
7467c458 85=item B<field.mysql_charset>, B<field.mysql_collate>
1ded8513 86
87Set the fields charater set and collation order.
88
5a0c7b43 89=back
90
c855a748 91=cut
92
9398955f 93use strict;
cd0ea0fd 94use warnings;
da5a1bae 95use vars qw[ $VERSION $DEBUG %used_names ];
866d012e 96$VERSION = sprintf "%d.%02d", q$Revision: 1.54 $ =~ /(\d+)\.(\d+)/;
5636ed00 97$DEBUG = 0 unless defined $DEBUG;
9398955f 98
99use Data::Dumper;
1c14e9f1 100use SQL::Translator::Schema::Constants;
5ee19df8 101use SQL::Translator::Utils qw(debug header_comment);
9398955f 102
d2344c83 103#
104# Use only lowercase for the keys (e.g. "long" and not "LONG")
105#
2620fc1c 106my %translate = (
107 #
108 # Oracle types
109 #
110 varchar2 => 'varchar',
111 long => 'text',
d2344c83 112 clob => 'longtext',
2620fc1c 113
114 #
115 # Sybase types
116 #
117 int => 'integer',
118 money => 'float',
119 real => 'double',
120 comment => 'text',
121 bit => 'tinyint',
87779799 122
123 #
124 # Access types
125 #
126 'long integer' => 'integer',
127 'text' => 'text',
128 'datetime' => 'datetime',
2620fc1c 129);
130
9ab59f87 131
132sub preprocess_schema {
934e1b39 133 my ($schema) = @_;
9ab59f87 134
135 # extra->{mysql_table_type} used to be the type. It belongs in options, so
136 # move it if we find it. Return Engine type if found in extra or options
7725e1e6 137 # Similarly for mysql_charset and mysql_collate
138 my $extra_to_options = sub {
139 my ($table, $extra_name, $opt_name) = @_;
9ab59f87 140
141 my $extra = $table->extra;
142
7725e1e6 143 my $extra_type = delete $extra->{$extra_name};
9ab59f87 144
145 # Now just to find if there is already an Engine or Type option...
146 # and lets normalize it to ENGINE since:
147 #
148 # The ENGINE table option specifies the storage engine for the table.
149 # TYPE is a synonym, but ENGINE is the preferred option name.
150 #
151
152 # We have to use the hash directly here since otherwise there is no way
153 # to remove options.
154 my $options = ( $table->{options} ||= []);
155
7725e1e6 156 # If multiple option names, normalize to the first one
157 if (ref $opt_name) {
158 OPT_NAME: for ( @$opt_name[1..$#$opt_name] ) {
159 for my $idx ( 0..$#{$options} ) {
160 my ($key, $value) = %{ $options->[$idx] };
161
162 if (uc $key eq $_) {
163 $options->[$idx] = { $opt_name->[0] => $value };
164 last OPT_NAME;
165 }
166 }
167 }
168 $opt_name = $opt_name->[0];
169
170 }
171
172
9ab59f87 173 # This assumes that there isn't both a Type and an Engine option.
7725e1e6 174 OPTION:
9ab59f87 175 for my $idx ( 0..$#{$options} ) {
176 my ($key, $value) = %{ $options->[$idx] };
177
7725e1e6 178 next unless uc $key eq $opt_name;
179
180 # make sure case is right on option name
9ab59f87 181 delete $options->[$idx]{$key};
7725e1e6 182 return $options->[$idx]{$opt_name} = $value || $extra_type;
9ab59f87 183
184 }
185
186 if ($extra_type) {
7725e1e6 187 push @$options, { $opt_name => $extra_type };
9ab59f87 188 return $extra_type;
189 }
190
191 };
192
1c680eb9 193 # Names are only specific to a given schema
194 local %used_names = ();
195
9ab59f87 196 #
197 # Work out which tables need to be InnoDB to support foreign key
198 # constraints. We do this first as we need InnoDB at both ends.
199 #
200 foreach my $table ( $schema->get_tables ) {
7725e1e6 201
202 $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE'] );
203 $extra_to_options->($table, 'mysql_charset', 'CHARACTER SET' );
204 $extra_to_options->($table, 'mysql_collate', 'COLLATE' );
9ab59f87 205
929ef265 206 foreach my $c ( $table->get_constraints ) {
1c680eb9 207 next unless $c->type eq FOREIGN_KEY;
208
209 # Normalize constraint names here.
210 my $c_name = $c->name;
211 # Give the constraint a name if it doesn't have one, so it doens't feel
212 # left out
213 $c_name = $table->name . '_fk' unless length $c_name;
214
215 $c->name( next_unused_name($c_name) );
216
9ab59f87 217 for my $meth (qw/table reference_table/) {
1c680eb9 218 my $table = $schema->get_table($c->$meth) || next;
7725e1e6 219 # This normalizes the types to ENGINE and returns the value if its there
220 next if $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE']);
9ab59f87 221 $table->options( { 'ENGINE' => 'InnoDB' } );
222 }
929ef265 223 } # foreach constraints
224
225 foreach my $f ( $table->get_fields ) {
226 my @size = $f->size;
227 if ( !$size[0] && $f->data_type =~ /char$/ ) {
228 $f->size( (255) );
229 }
9ab59f87 230 }
929ef265 231
9ab59f87 232 }
233}
234
9398955f 235sub produce {
a1d94525 236 my $translator = shift;
237 local $DEBUG = $translator->debug;
da5a1bae 238 local %used_names;
a1d94525 239 my $no_comments = $translator->no_comments;
240 my $add_drop_table = $translator->add_drop_table;
241 my $schema = $translator->schema;
2bc23e82 242 my $show_warnings = $translator->show_warnings || 0;
d529894e 243
7725e1e6 244 my ($qt, $qf, $qc) = ('','', '');
fe0f47d0 245 $qt = '`' if $translator->quote_table_names;
246 $qf = '`' if $translator->quote_field_names;
247
1a24938d 248 debug("PKG: Beginning production\n");
da5a1bae 249 %used_names = ();
d529894e 250 my $create;
5ee19df8 251 $create .= header_comment unless ($no_comments);
0823773d 252 # \todo Don't set if MySQL 3.x is set on command line
da147d03 253 $create .= "SET foreign_key_checks=0;\n\n";
9398955f 254
934e1b39 255 preprocess_schema($schema);
5a0c7b43 256
257 #
258 # Generate sql
259 #
cd0ea0fd 260 my @table_defs =();
4d438549 261
1c14e9f1 262 for my $table ( $schema->get_tables ) {
cd0ea0fd 263# print $table->name, "\n";
0013ee25 264 push @table_defs, create_table($table,
fe0f47d0 265 { add_drop_table => $add_drop_table,
266 show_warnings => $show_warnings,
267 no_comments => $no_comments,
268 quote_table_names => $qt,
269 quote_field_names => $qf
0013ee25 270 });
271 }
9398955f 272
cd0ea0fd 273# print "@table_defs\n";
fa94b25f 274 push @table_defs, "SET foreign_key_checks=1;\n\n";
cd0ea0fd 275
fa94b25f 276 return wantarray ? ($create, @table_defs) : $create . join ('', @table_defs);
0013ee25 277}
9398955f 278
0013ee25 279sub create_table
280{
281 my ($table, $options) = @_;
2620fc1c 282
fe0f47d0 283 my $qt = $options->{quote_table_names} || '';
284 my $qf = $options->{quote_field_names} || '';
285
0013ee25 286 my $table_name = $table->name;
287 debug("PKG: Looking at table '$table_name'\n");
6d3f6379 288
0013ee25 289 #
290 # Header. Should this look like what mysqldump produces?
291 #
cd0ea0fd 292 my $create = '';
fa94b25f 293 my $drop;
fe0f47d0 294 $create .= "--\n-- Table: $qt$table_name$qt\n--\n" unless $options->{no_comments};
fa94b25f 295 $drop = qq[DROP TABLE IF EXISTS $qt$table_name$qt;\n] if $options->{add_drop_table};
fe0f47d0 296 $create .= "CREATE TABLE $qt$table_name$qt (\n";
2b695517 297
0013ee25 298 #
299 # Fields
300 #
301 my @field_defs;
302 for my $field ( $table->get_fields ) {
fe0f47d0 303 push @field_defs, create_field($field, $options);
0013ee25 304 }
1ded8513 305
0013ee25 306 #
307 # Indices
308 #
309 my @index_defs;
310 my %indexed_fields;
311 for my $index ( $table->get_indices ) {
fe0f47d0 312 push @index_defs, create_index($index, $options);
0013ee25 313 $indexed_fields{ $_ } = 1 for $index->fields;
314 }
d529894e 315
0013ee25 316 #
317 # Constraints -- need to handle more than just FK. -ky
318 #
319 my @constraint_defs;
320 my @constraints = $table->get_constraints;
321 for my $c ( @constraints ) {
cd0ea0fd 322 my $constr = create_constraint($c, $options);
323 push @constraint_defs, $constr if($constr);
0013ee25 324
da5a1bae 325 unless ( $indexed_fields{ ($c->fields())[0] } || $c->type ne FOREIGN_KEY ) {
326 push @index_defs, "INDEX ($qf" . ($c->fields())[0] . "$qf)";
327 $indexed_fields{ ($c->fields())[0] } = 1;
328 }
0013ee25 329 }
1ded8513 330
0013ee25 331 $create .= join(",\n", map { " $_" }
332 @field_defs, @index_defs, @constraint_defs
333 );
9398955f 334
0013ee25 335 #
336 # Footer
337 #
338 $create .= "\n)";
da5a1bae 339 $create .= generate_table_options($table) || '';
0013ee25 340 $create .= ";\n\n";
9398955f 341
fa94b25f 342 return $drop ? ($drop,$create) : $create;
0013ee25 343}
f8b6e804 344
da5a1bae 345sub generate_table_options
346{
347 my ($table) = @_;
348 my $create;
349
350 my $table_type_defined = 0;
7725e1e6 351 my $charset = $table->extra('mysql_charset');
352 my $collate = $table->extra('mysql_collate');
da5a1bae 353 for my $t1_option_ref ( $table->options ) {
354 my($key, $value) = %{$t1_option_ref};
355 $table_type_defined = 1
356 if uc $key eq 'ENGINE' or uc $key eq 'TYPE';
7725e1e6 357 if (uc $key eq 'CHARACTER SET') {
358 $charset = $value;
359 next;
360 } elsif (uc $key eq 'COLLATE') {
361 $collate = $value;
362 next;
363 }
da5a1bae 364 $create .= " $key=$value";
365 }
9ab59f87 366
da5a1bae 367 my $mysql_table_type = $table->extra('mysql_table_type');
9ab59f87 368 $create .= " ENGINE=$mysql_table_type"
da5a1bae 369 if $mysql_table_type && !$table_type_defined;
da5a1bae 370 my $comments = $table->comments;
371
372 $create .= " DEFAULT CHARACTER SET $charset" if $charset;
373 $create .= " COLLATE $collate" if $collate;
374 $create .= qq[ comment='$comments'] if $comments;
375 return $create;
376}
377
0013ee25 378sub create_field
379{
fe0f47d0 380 my ($field, $options) = @_;
381
382 my $qf = $options->{quote_field_names} ||= '';
9398955f 383
0013ee25 384 my $field_name = $field->name;
385 debug("PKG: Looking at field '$field_name'\n");
fe0f47d0 386 my $field_def = "$qf$field_name$qf";
0013ee25 387
388 # data type and size
389 my $data_type = $field->data_type;
390 my @size = $field->size;
391 my %extra = $field->extra;
392 my $list = $extra{'list'} || [];
393 # \todo deal with embedded quotes
394 my $commalist = join( ', ', map { qq['$_'] } @$list );
395 my $charset = $extra{'mysql_charset'};
396 my $collate = $extra{'mysql_collate'};
397
398 #
399 # Oracle "number" type -- figure best MySQL type
400 #
401 if ( lc $data_type eq 'number' ) {
402 # not an integer
403 if ( scalar @size > 1 ) {
404 $data_type = 'double';
d529894e 405 }
0013ee25 406 elsif ( $size[0] && $size[0] >= 12 ) {
407 $data_type = 'bigint';
408 }
409 elsif ( $size[0] && $size[0] <= 1 ) {
410 $data_type = 'tinyint';
411 }
412 else {
413 $data_type = 'int';
414 }
415 }
416 #
417 # Convert a large Oracle varchar to "text"
418 #
419 elsif ( $data_type =~ /char/i && $size[0] > 255 ) {
420 $data_type = 'text';
421 @size = ();
422 }
0013ee25 423 elsif ( $data_type =~ /boolean/i ) {
424 $data_type = 'enum';
425 $commalist = "'0','1'";
426 }
427 elsif ( exists $translate{ lc $data_type } ) {
428 $data_type = $translate{ lc $data_type };
429 }
430
431 @size = () if $data_type =~ /(text|blob)/i;
432
433 if ( $data_type =~ /(double|float)/ && scalar @size == 1 ) {
434 push @size, '0';
435 }
d529894e 436
0013ee25 437 $field_def .= " $data_type";
438
439 if ( lc $data_type eq 'enum' ) {
440 $field_def .= '(' . $commalist . ')';
441 }
442 elsif ( defined $size[0] && $size[0] > 0 ) {
443 $field_def .= '(' . join( ', ', @size ) . ')';
444 }
445
446 # char sets
447 $field_def .= " CHARACTER SET $charset" if $charset;
448 $field_def .= " COLLATE $collate" if $collate;
449
450 # MySQL qualifiers
451 for my $qual ( qw[ binary unsigned zerofill ] ) {
452 my $val = $extra{ $qual } || $extra{ uc $qual } or next;
453 $field_def .= " $qual";
454 }
455 for my $qual ( 'character set', 'collate', 'on update' ) {
456 my $val = $extra{ $qual } || $extra{ uc $qual } or next;
457 $field_def .= " $qual $val";
458 }
459
460 # Null?
461 $field_def .= ' NOT NULL' unless $field->is_nullable;
462
463 # Default? XXX Need better quoting!
464 my $default = $field->default_value;
465 if ( defined $default ) {
466 if ( uc $default eq 'NULL') {
467 $field_def .= ' DEFAULT NULL';
468 } else {
469 $field_def .= " DEFAULT '$default'";
470 }
471 }
472
473 if ( my $comments = $field->comments ) {
474 $field_def .= qq[ comment '$comments'];
475 }
476
477 # auto_increment?
478 $field_def .= " auto_increment" if $field->is_auto_increment;
479
480 return $field_def;
481}
482
da5a1bae 483sub alter_create_index
484{
485 my ($index, $options) = @_;
486
487 my $qt = $options->{quote_table_names} || '';
488 my $qf = $options->{quote_field_names} || '';
489
490 return join( ' ',
491 'ALTER TABLE',
492 $qt.$index->table->name.$qt,
493 'ADD',
494 create_index(@_)
495 );
496}
497
0013ee25 498sub create_index
499{
fe0f47d0 500 my ($index, $options) = @_;
501
502 my $qf = $options->{quote_field_names} || '';
0013ee25 503
504 return join( ' ',
da5a1bae 505 lc $index->type eq 'normal' ? 'INDEX' : $index->type . ' INDEX',
0013ee25 506 $index->name,
fe0f47d0 507 '(' . $qf . join( "$qf, $qf", $index->fields ) . $qf . ')'
0013ee25 508 );
509
510}
511
da5a1bae 512sub alter_drop_index
513{
514 my ($index, $options) = @_;
515
516 my $qt = $options->{quote_table_names} || '';
517 my $qf = $options->{quote_field_names} || '';
518
519 return join( ' ',
520 'ALTER TABLE',
521 $qt.$index->table->name.$qt,
522 'DROP',
523 'INDEX',
524 $index->name || $index->fields
525 );
526
527}
528
529sub alter_drop_constraint
530{
531 my ($c, $options) = @_;
532
533 my $qt = $options->{quote_table_names} || '';
7725e1e6 534 my $qc = $options->{quote_field_names} || '';
da5a1bae 535
536 my $out = sprintf('ALTER TABLE %s DROP %s %s',
7467c458 537 $qt . $c->table->name . $qt,
74ca32ce 538 $c->type eq FOREIGN_KEY ? $c->type : "INDEX",
da5a1bae 539 $qc . $c->name . $qc );
540
541 return $out;
542}
543
544sub alter_create_constraint
545{
546 my ($index, $options) = @_;
547
548 my $qt = $options->{quote_table_names} || '';
549 return join( ' ',
550 'ALTER TABLE',
551 $qt.$index->table->name.$qt,
552 'ADD',
553 create_constraint(@_) );
554}
555
0013ee25 556sub create_constraint
557{
558 my ($c, $options) = @_;
559
fb149f81 560 my $qf = $options->{quote_field_names} || '';
561 my $qt = $options->{quote_table_names} || '';
da5a1bae 562 my $leave_name = $options->{leave_name} || undef;
fe0f47d0 563
0013ee25 564 my @fields = $c->fields or next;
565
566 if ( $c->type eq PRIMARY_KEY ) {
fe0f47d0 567 return 'PRIMARY KEY (' . $qf . join("$qf, $qf", @fields). $qf . ')';
0013ee25 568 }
569 elsif ( $c->type eq UNIQUE ) {
570 return
fe0f47d0 571 'UNIQUE '.
572 (defined $c->name ? $qf.$c->name.$qf.' ' : '').
573 '(' . $qf . join("$qf, $qf", @fields). $qf . ')';
0013ee25 574 }
575 elsif ( $c->type eq FOREIGN_KEY ) {
d529894e 576 #
0013ee25 577 # Make sure FK field is indexed or MySQL complains.
5e56da9a 578 #
0013ee25 579
866d012e 580 my $table = $c->table;
da5a1bae 581 my $c_name = $c->name;
866d012e 582
0013ee25 583 my $def = join(' ',
fb149f81 584 map { $_ || () }
585 'CONSTRAINT',
7725e1e6 586 $qf . $c_name . $qf,
fb149f81 587 'FOREIGN KEY'
588 );
0013ee25 589
da5a1bae 590
fe0f47d0 591 $def .= ' ('.$qf . join( "$qf, $qf", @fields ) . $qf . ')';
0013ee25 592
fe0f47d0 593 $def .= ' REFERENCES ' . $qt . $c->reference_table . $qt;
0013ee25 594
595 my @rfields = map { $_ || () } $c->reference_fields;
596 unless ( @rfields ) {
597 my $rtable_name = $c->reference_table;
866d012e 598 if ( my $ref_table = $table->schema->get_table( $rtable_name ) ) {
0013ee25 599 push @rfields, $ref_table->primary_key;
1c14e9f1 600 }
0013ee25 601 else {
602 warn "Can't find reference table '$rtable_name' " .
603 "in schema\n" if $options->{show_warnings};
5e56da9a 604 }
605 }
606
0013ee25 607 if ( @rfields ) {
fe0f47d0 608 $def .= ' (' . $qf . join( "$qf, $qf", @rfields ) . $qf . ')';
0013ee25 609 }
610 else {
866d012e 611 warn "FK constraint on " . $table->name . '.' .
0013ee25 612 join('', @fields) . " has no reference fields\n"
613 if $options->{show_warnings};
614 }
5e56da9a 615
0013ee25 616 if ( $c->match_type ) {
617 $def .= ' MATCH ' .
618 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
619 }
620
621 if ( $c->on_delete ) {
622 $def .= ' ON DELETE '.join( ' ', $c->on_delete );
623 }
624
625 if ( $c->on_update ) {
626 $def .= ' ON UPDATE '.join( ' ', $c->on_update );
627 }
cd0ea0fd 628 return $def;
9398955f 629 }
630
cd0ea0fd 631 return undef;
0013ee25 632}
633
da5a1bae 634sub alter_table
635{
636 my ($to_table, $options) = @_;
637
7467c458 638 my $qt = $options->{quote_table_names} || '';
da5a1bae 639
640 my $table_options = generate_table_options($to_table) || '';
641 my $out = sprintf('ALTER TABLE %s%s',
642 $qt . $to_table->name . $qt,
643 $table_options);
644
645 return $out;
646}
647
4d438549 648sub rename_field { alter_field(@_) }
0013ee25 649sub alter_field
650{
fe0f47d0 651 my ($from_field, $to_field, $options) = @_;
652
7467c458 653 my $qf = $options->{quote_field_names} || '';
654 my $qt = $options->{quote_table_names} || '';
0013ee25 655
656 my $out = sprintf('ALTER TABLE %s CHANGE COLUMN %s %s',
fe0f47d0 657 $qt . $to_field->table->name . $qt,
4d438549 658 $qf . $from_field->name . $qf,
fe0f47d0 659 create_field($to_field, $options));
0013ee25 660
661 return $out;
662}
663
664sub add_field
665{
fe0f47d0 666 my ($new_field, $options) = @_;
667
7467c458 668 my $qt = $options->{quote_table_names} || '';
0013ee25 669
670 my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
fe0f47d0 671 $qt . $new_field->table->name . $qt,
672 create_field($new_field, $options));
0013ee25 673
674 return $out;
675
676}
677
678sub drop_field
679{
fe0f47d0 680 my ($old_field, $options) = @_;
0013ee25 681
7467c458 682 my $qf = $options->{quote_field_names} || '';
683 my $qt = $options->{quote_table_names} || '';
fe0f47d0 684
0013ee25 685 my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
fe0f47d0 686 $qt . $old_field->table->name . $qt,
687 $qf . $old_field->name . $qf);
0013ee25 688
689 return $out;
690
9398955f 691}
692
4d438549 693sub batch_alter_table {
694 my ($table, $diff_hash, $options) = @_;
695
f9ed5d54 696 # InnoDB has an issue with dropping and re-adding a FK constraint under the
697 # name in a single alter statment, see: http://bugs.mysql.com/bug.php?id=13741
698 #
699 # We have to work round this.
700
701 my %fks_to_alter;
702 my %fks_to_drop = map {
703 $_->type eq FOREIGN_KEY
704 ? ( $_->name => $_ )
705 : ( )
706 } @{$diff_hash->{alter_drop_constraint} };
707
708 my %fks_to_create = map {
709 if ( $_->type eq FOREIGN_KEY) {
710 $fks_to_alter{$_->name} = $fks_to_drop{$_->name} if $fks_to_drop{$_->name};
711 ( $_->name => $_ );
712 } else { ( ) }
713 } @{$diff_hash->{alter_create_constraint} };
714
715 my $drop_stmt = '';
716 if (scalar keys %fks_to_alter) {
717 $diff_hash->{alter_drop_constraint} = [
718 grep { !$fks_to_alter{$_->name} } @{ $diff_hash->{alter_drop_constraint} }
719 ];
720
721 $drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options)
722 . "\n";
723
724 }
725
4d438549 726 my @stmts = map {
727 if (@{ $diff_hash->{$_} || [] }) {
728 my $meth = __PACKAGE__->can($_) or die __PACKAGE__ . " cant $_";
f9ed5d54 729 map { $meth->( (ref $_ eq 'ARRAY' ? @$_ : $_), $options ) } @{ $diff_hash->{$_} }
4d438549 730 } else { () }
46bf5655 731 } qw/rename_table
732 alter_drop_constraint
4d438549 733 alter_drop_index
734 drop_field
735 add_field
736 alter_field
737 rename_field
738 alter_create_index
739 alter_create_constraint
740 alter_table/;
741
46bf5655 742 # rename_table makes things a bit more complex
743 my $renamed_from = "";
744 $renamed_from = $diff_hash->{rename_table}[0][0]->name
745 if $diff_hash->{rename_table} && @{$diff_hash->{rename_table}};
746
4d438549 747 return unless @stmts;
748 # Just zero or one stmts. return now
f9ed5d54 749 return "$drop_stmt@stmts;" unless @stmts > 1;
4d438549 750
751 # Now strip off the 'ALTER TABLE xyz' of all but the first one
752
7467c458 753 my $qt = $options->{quote_table_names} || '';
4104f82b 754 my $table_name = $qt . $table->name . $qt;
755
756
757 my $re = $renamed_from
758 ? qr/^ALTER TABLE (?:\Q$table_name\E|\Q$qt$renamed_from$qt\E) /
759 : qr/^ALTER TABLE \Q$table_name\E /;
4d438549 760
761 my $first = shift @stmts;
4104f82b 762 my ($alter_table) = $first =~ /($re)/;
46bf5655 763
4d438549 764 my $padd = " " x length($alter_table);
765
f9ed5d54 766 return $drop_stmt . join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts) . ';';
4104f82b 767
4d438549 768}
769
770sub drop_table {
46bf5655 771 my ($table, $options) = @_;
772
773 my $qt = $options->{quote_table_names} || '';
4d438549 774
775 # Drop (foreign key) constraints so table drops cleanly
46bf5655 776 my @sql = batch_alter_table($table, { alter_drop_constraint => [ grep { $_->type eq 'FOREIGN KEY' } $table->get_constraints ] }, $options);
777
778 return join("\n", @sql, "DROP TABLE $qt$table$qt;");
779
780}
781
782sub rename_table {
783 my ($old_table, $new_table, $options) = @_;
4d438549 784
46bf5655 785 my $qt = $options->{quote_table_names} || '';
4d438549 786
46bf5655 787 return "ALTER TABLE $qt$old_table$qt RENAME TO $qt$new_table$qt";
4d438549 788}
789
da5a1bae 790sub next_unused_name {
791 my $name = shift || '';
792 if ( !defined($used_names{$name}) ) {
793 $used_names{$name} = $name;
794 return $name;
795 }
796
797 my $i = 1;
798 while ( defined($used_names{$name . '_' . $i}) ) {
799 ++$i;
800 }
801 $name .= '_' . $i;
802 $used_names{$name} = $name;
803 return $name;
804}
805
9398955f 8061;
9398955f 807
c855a748 808# -------------------------------------------------------------------
9398955f 809
c855a748 810=pod
811
812=head1 SEE ALSO
813
814SQL::Translator, http://www.mysql.com/.
9398955f 815
2d6979da 816=head1 AUTHORS
9398955f 817
758ab1cd 818darren chamberlain E<lt>darren@cpan.orgE<gt>,
c855a748 819Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
820
821=cut