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