Merge 'trunk' into 'roundtrip'
[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_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 # Column types that do not support lenth attribute
150 #
151 my @no_length_attr = qw/
152   date time timestamp datetime year
153   /;
154
155
156 sub preprocess_schema {
157     my ($schema) = @_;
158
159     # extra->{mysql_table_type} used to be the type. It belongs in options, so
160     # move it if we find it. Return Engine type if found in extra or options
161     # Similarly for mysql_charset and mysql_collate
162     my $extra_to_options = sub {
163       my ($table, $extra_name, $opt_name) = @_;
164
165       my $extra = $table->extra;
166
167       my $extra_type = delete $extra->{$extra_name};
168
169       # Now just to find if there is already an Engine or Type option...
170       # and lets normalize it to ENGINE since:
171       #
172       # The ENGINE table option specifies the storage engine for the table. 
173       # TYPE is a synonym, but ENGINE is the preferred option name.
174       #
175
176       # We have to use the hash directly here since otherwise there is no way 
177       # to remove options.
178       my $options = ( $table->{options} ||= []);
179
180       # If multiple option names, normalize to the first one
181       if (ref $opt_name) {
182         OPT_NAME: for ( @$opt_name[1..$#$opt_name] ) {
183           for my $idx ( 0..$#{$options} ) {
184             my ($key, $value) = %{ $options->[$idx] };
185             
186             if (uc $key eq $_) {
187               $options->[$idx] = { $opt_name->[0] => $value };
188               last OPT_NAME;
189             }
190           }
191         }
192         $opt_name = $opt_name->[0];
193
194       }
195
196
197       # This assumes that there isn't both a Type and an Engine option.
198       OPTION:
199       for my $idx ( 0..$#{$options} ) {
200         my ($key, $value) = %{ $options->[$idx] };
201
202         next unless uc $key eq $opt_name;
203      
204         # make sure case is right on option name
205         delete $options->[$idx]{$key};
206         return $options->[$idx]{$opt_name} = $value || $extra_type;
207
208       }
209   
210       if ($extra_type) {
211         push @$options, { $opt_name => $extra_type };
212         return $extra_type;
213       }
214
215     };
216
217     # Names are only specific to a given schema
218     local %used_names = ();
219
220     #
221     # Work out which tables need to be InnoDB to support foreign key
222     # constraints. We do this first as we need InnoDB at both ends.
223     #
224     foreach my $table ( $schema->get_tables ) {
225       
226         $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE'] );
227         $extra_to_options->($table, 'mysql_charset', 'CHARACTER SET' );
228         $extra_to_options->($table, 'mysql_collate', 'COLLATE' );
229
230         foreach my $c ( $table->get_constraints ) {
231             next unless $c->type eq FOREIGN_KEY;
232
233             # Normalize constraint names here.
234             my $c_name = $c->name;
235             # Give the constraint a name if it doesn't have one, so it doens't feel
236             # left out
237             $c_name   = $table->name . '_fk' unless length $c_name;
238             
239             $c->name( next_unused_name($c_name) );
240
241             for my $meth (qw/table reference_table/) {
242                 my $table = $schema->get_table($c->$meth) || next;
243                 # This normalizes the types to ENGINE and returns the value if its there
244                 next if $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE']);
245                 $table->options( { 'ENGINE' => 'InnoDB' } );
246             }
247         } # foreach constraints
248
249         my %map = ( mysql_collate => 'collate', mysql_charset => 'character set');
250         foreach my $f ( $table->get_fields ) {
251           my $extra = $f->extra;
252           for (keys %map) {
253             $extra->{$map{$_}} = delete $extra->{$_} if exists $extra->{$_};
254           }
255
256           my @size = $f->size;
257           if ( !$size[0] && $f->data_type =~ /char$/ ) {
258             $f->size( (255) );
259           }
260         }
261
262     }
263 }
264
265 sub produce {
266     my $translator     = shift;
267     local $DEBUG       = $translator->debug;
268     local %used_names;
269     my $no_comments    = $translator->no_comments;
270     my $add_drop_table = $translator->add_drop_table;
271     my $schema         = $translator->schema;
272     my $show_warnings  = $translator->show_warnings || 0;
273     my $producer_args  = $translator->producer_args;
274     my $mysql_version  = parse_mysql_version ($producer_args->{mysql_version}, 'perl') || 0;
275     my $max_id_length  = $producer_args->{mysql_max_id_length} || $DEFAULT_MAX_ID_LENGTH;
276
277     my ($qt, $qf, $qc) = ('','', '');
278     $qt = '`' if $translator->quote_table_names;
279     $qf = '`' if $translator->quote_field_names;
280
281     debug("PKG: Beginning production\n");
282     %used_names = ();
283     my $create = ''; 
284     $create .= header_comment unless ($no_comments);
285     # \todo Don't set if MySQL 3.x is set on command line
286     my @create = "SET foreign_key_checks=0";
287
288     preprocess_schema($schema);
289
290     #
291     # Generate sql
292     #
293     my @table_defs =();
294     
295     for my $table ( $schema->get_tables ) {
296 #        print $table->name, "\n";
297         push @table_defs, create_table($table, 
298                                        { add_drop_table    => $add_drop_table,
299                                          show_warnings     => $show_warnings,
300                                          no_comments       => $no_comments,
301                                          quote_table_names => $qt,
302                                          quote_field_names => $qf,
303                                          max_id_length     => $max_id_length,
304                                          mysql_version     => $mysql_version
305                                          });
306     }
307
308     if ($mysql_version >= 5.000001) {
309       for my $view ( $schema->get_views ) {
310         push @table_defs, create_view($view,
311                                        { add_replace_view  => $add_drop_table,
312                                          show_warnings     => $show_warnings,
313                                          no_comments       => $no_comments,
314                                          quote_table_names => $qt,
315                                          quote_field_names => $qf,
316                                          max_id_length     => $max_id_length,
317                                          mysql_version     => $mysql_version
318                                          });
319       }
320     }
321
322
323 #    print "@table_defs\n";
324     push @table_defs, "SET foreign_key_checks=1";
325
326     return wantarray ? ($create ? $create : (), @create, @table_defs) : ($create . join('', map { $_ ? "$_;\n\n" : () } (@create, @table_defs)));
327 }
328
329 sub create_view {
330     my ($view, $options) = @_;
331     my $qt = $options->{quote_table_names} || '';
332     my $qf = $options->{quote_field_names} || '';
333
334     my $view_name = $view->name;
335     debug("PKG: Looking at view '${view_name}'\n");
336
337     # Header.  Should this look like what mysqldump produces?
338     my $create = '';
339     $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n" unless $options->{no_comments};
340     $create .= 'CREATE';
341     $create .= ' OR REPLACE' if $options->{add_replace_view};
342     $create .= "\n";
343
344     my $extra = $view->extra;
345     # ALGORITHM
346     if( exists($extra->{mysql_algorithm}) && defined(my $algorithm = $extra->{mysql_algorithm}) ){
347       $create .= "   ALGORITHM = ${algorithm}\n" if $algorithm =~ /(?:UNDEFINED|MERGE|TEMPTABLE)/i;
348     }
349     # DEFINER
350     if( exists($extra->{mysql_definer}) && defined(my $user = $extra->{mysql_definer}) ){
351       $create .= "   DEFINER = ${user}\n";
352     }
353     # SECURITY
354     if( exists($extra->{mysql_security}) && defined(my $security = $extra->{mysql_security}) ){
355       $create .= "   SQL SECURITY ${security}\n" if $security =~ /(?:DEFINER|INVOKER)/i;
356     }
357
358     #Header, cont.
359     $create .= "  VIEW ${qt}${view_name}${qt}";
360
361     if( my @fields = $view->fields ){
362       my $list = join ', ', map { "${qf}${_}${qf}"} @fields;
363       $create .= " ( ${list} )";
364     }
365     if( my $sql = $view->sql ){
366       $create .= " AS (\n    ${sql}\n  )";
367     }
368 #    $create .= "";
369     return $create;
370 }
371
372 sub create_table
373 {
374     my ($table, $options) = @_;
375
376     my $qt = $options->{quote_table_names} || '';
377     my $qf = $options->{quote_field_names} || '';
378
379     my $table_name = quote_table_name($table->name, $qt);
380     debug("PKG: Looking at table '$table_name'\n");
381
382     #
383     # Header.  Should this look like what mysqldump produces?
384     #
385     my $create = '';
386     my $drop;
387     $create .= "--\n-- Table: $table_name\n--\n" unless $options->{no_comments};
388     $drop = qq[DROP TABLE IF EXISTS $table_name] if $options->{add_drop_table};
389     $create .= "CREATE TABLE $table_name (\n";
390
391     #
392     # Fields
393     #
394     my @field_defs;
395     for my $field ( $table->get_fields ) {
396         push @field_defs, create_field($field, $options);
397     }
398
399     #
400     # Indices
401     #
402     my @index_defs;
403     my %indexed_fields;
404     for my $index ( $table->get_indices ) {
405         push @index_defs, create_index($index, $options);
406         $indexed_fields{ $_ } = 1 for $index->fields;
407     }
408
409     #
410     # Constraints -- need to handle more than just FK. -ky
411     #
412     my @constraint_defs;
413     my @constraints = $table->get_constraints;
414     for my $c ( @constraints ) {
415         my $constr = create_constraint($c, $options);
416         push @constraint_defs, $constr if($constr);
417         
418          unless ( $indexed_fields{ ($c->fields())[0] } || $c->type ne FOREIGN_KEY ) {
419              push @index_defs, "INDEX ($qf" . ($c->fields())[0] . "$qf)";
420              $indexed_fields{ ($c->fields())[0] } = 1;
421          }
422     }
423
424     $create .= join(",\n", map { "  $_" } 
425                     @field_defs, @index_defs, @constraint_defs
426                     );
427
428     #
429     # Footer
430     #
431     $create .= "\n)";
432     $create .= generate_table_options($table, $options) || '';
433 #    $create .= ";\n\n";
434
435     return $drop ? ($drop,$create) : $create;
436 }
437
438 sub quote_table_name {
439   my ($table_name, $qt) = @_;
440
441   $table_name =~ s/\./$qt.$qt/g;
442
443   return "$qt$table_name$qt";
444 }
445
446 sub generate_table_options 
447 {
448   my ($table, $options) = @_;
449   my $create;
450
451   my $table_type_defined = 0;
452   my $qf               = $options->{quote_field_names} ||= '';
453   my $charset          = $table->extra('mysql_charset');
454   my $collate          = $table->extra('mysql_collate');
455   my $union            = undef;
456   for my $t1_option_ref ( $table->options ) {
457     my($key, $value) = %{$t1_option_ref};
458     $table_type_defined = 1
459       if uc $key eq 'ENGINE' or uc $key eq 'TYPE';
460     if (uc $key eq 'CHARACTER SET') {
461       $charset = $value;
462       next;
463     } elsif (uc $key eq 'COLLATE') {
464       $collate = $value;
465       next;
466     } elsif (uc $key eq 'UNION') {
467       $union = "($qf". join("$qf, $qf", @$value) ."$qf)";
468       next;
469     }
470     $create .= " $key=$value";
471   }
472
473   my $mysql_table_type = $table->extra('mysql_table_type');
474   $create .= " ENGINE=$mysql_table_type"
475     if $mysql_table_type && !$table_type_defined;
476   my $comments         = $table->comments;
477
478   $create .= " DEFAULT CHARACTER SET $charset" if $charset;
479   $create .= " COLLATE $collate" if $collate;
480   $create .= " UNION=$union" if $union;
481   $create .= qq[ comment='$comments'] if $comments;
482   return $create;
483 }
484
485 sub create_field
486 {
487     my ($field, $options) = @_;
488
489     my $qf = $options->{quote_field_names} ||= '';
490
491     my $field_name = $field->name;
492     debug("PKG: Looking at field '$field_name'\n");
493     my $field_def = "$qf$field_name$qf";
494
495     # data type and size
496     my $data_type = $field->data_type;
497     my @size      = $field->size;
498     my %extra     = $field->extra;
499     my $list      = $extra{'list'} || [];
500     # \todo deal with embedded quotes
501     my $commalist = join( ', ', map { qq['$_'] } @$list );
502     my $charset = $extra{'mysql_charset'};
503     my $collate = $extra{'mysql_collate'};
504
505     my $mysql_version = $options->{mysql_version} || 0;
506     #
507     # Oracle "number" type -- figure best MySQL type
508     #
509     if ( lc $data_type eq 'number' ) {
510         # not an integer
511         if ( scalar @size > 1 ) {
512             $data_type = 'double';
513         }
514         elsif ( $size[0] && $size[0] >= 12 ) {
515             $data_type = 'bigint';
516         }
517         elsif ( $size[0] && $size[0] <= 1 ) {
518             $data_type = 'tinyint';
519         }
520         else {
521             $data_type = 'int';
522         }
523     }
524     #
525     # Convert a large Oracle varchar to "text"
526     # (not necessary as of 5.0.3 http://dev.mysql.com/doc/refman/5.0/en/char.html)
527     #
528     elsif ( $data_type =~ /char/i && $size[0] > 255 ) {
529         unless ($size[0] <= 65535 && $mysql_version >= 5.000003 ) {
530             $data_type = 'text';
531             @size      = ();
532         }
533     }
534     elsif ( $data_type =~ /boolean/i ) {
535         if ($mysql_version >= 4) {
536             $data_type = 'boolean';
537         } else {
538             $data_type = 'enum';
539             $commalist = "'0','1'";
540         }
541     }
542     elsif ( exists $translate{ lc $data_type } ) {
543         $data_type = $translate{ lc $data_type };
544     }
545
546     @size = () if $data_type =~ /(text|blob)/i;
547
548     if ( $data_type =~ /(double|float)/ && scalar @size == 1 ) {
549         push @size, '0';
550     }
551
552     $field_def .= " $data_type";
553
554     if ( lc($data_type) eq 'enum' || lc($data_type) eq 'set') {
555         $field_def .= '(' . $commalist . ')';
556     }
557     elsif ( defined $size[0] && $size[0] > 0 && ! grep $data_type eq $_, @no_length_attr  ) {
558         $field_def .= '(' . join( ', ', @size ) . ')';
559     }
560
561     # char sets
562     $field_def .= " CHARACTER SET $charset" if $charset;
563     $field_def .= " COLLATE $collate" if $collate;
564
565     # MySQL qualifiers
566     for my $qual ( qw[ binary unsigned zerofill ] ) {
567         my $val = $extra{ $qual } || $extra{ uc $qual } or next;
568         $field_def .= " $qual";
569     }
570     for my $qual ( 'character set', 'collate', 'on update' ) {
571         my $val = $extra{ $qual } || $extra{ uc $qual } or next;
572         $field_def .= " $qual $val";
573     }
574
575     # Null?
576     $field_def .= ' NOT NULL' unless $field->is_nullable;
577
578     # Default?  XXX Need better quoting!
579     my $default = $field->default_value;
580     if ( defined $default ) {
581         SQL::Translator::Producer->_apply_default_value(
582           \$field_def,
583           $default, 
584           [
585             'NULL'       => \'NULL',
586           ],
587         );
588     }
589
590     if ( my $comments = $field->comments ) {
591         $field_def .= qq[ comment '$comments'];
592     }
593
594     # auto_increment?
595     $field_def .= " auto_increment" if $field->is_auto_increment;
596
597     return $field_def;
598 }
599
600 sub alter_create_index
601 {
602     my ($index, $options) = @_;
603
604     my $qt = $options->{quote_table_names} || '';
605     my $qf = $options->{quote_field_names} || '';
606
607     return join( ' ',
608                  'ALTER TABLE',
609                  $qt.$index->table->name.$qt,
610                  'ADD',
611                  create_index(@_)
612                  );
613 }
614
615 sub create_index
616 {
617     my ($index, $options) = @_;
618
619     my $qf = $options->{quote_field_names} || '';
620
621     return join( ' ', 
622                   lc $index->type eq 'normal' 
623                     ? 'INDEX' 
624                     : $index->type . ' INDEX'
625                   ,
626                   $index->name 
627                     ? (truncate_id_uniquely( $index->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH ) )
628                     : ()
629                   ,
630                   '(' . $qf . join( "$qf, $qf", $index->fields ) . $qf . ')'
631                  );
632
633 }
634
635 sub alter_drop_index
636 {
637     my ($index, $options) = @_;
638
639     my $qt = $options->{quote_table_names} || '';
640     my $qf = $options->{quote_field_names} || '';
641
642     return join( ' ', 
643                  'ALTER TABLE',
644                  $qt.$index->table->name.$qt,
645                  'DROP',
646                  'INDEX',
647                  $index->name || $index->fields
648                  );
649
650 }
651
652 sub alter_drop_constraint
653 {
654     my ($c, $options) = @_;
655
656     my $qt      = $options->{quote_table_names} || '';
657     my $qc      = $options->{quote_field_names} || '';
658
659     my $out = sprintf('ALTER TABLE %s DROP %s %s',
660                       $qt . $c->table->name . $qt,
661                       $c->type eq FOREIGN_KEY ? $c->type : "INDEX",
662                       $qc . $c->name . $qc );
663
664     return $out;
665 }
666
667 sub alter_create_constraint
668 {
669     my ($index, $options) = @_;
670
671     my $qt = $options->{quote_table_names} || '';
672     return join( ' ',
673                  'ALTER TABLE',
674                  $qt.$index->table->name.$qt,
675                  'ADD',
676                  create_constraint(@_) );
677 }
678
679 sub create_constraint
680 {
681     my ($c, $options) = @_;
682
683     my $qf      = $options->{quote_field_names} || '';
684     my $qt      = $options->{quote_table_names} || '';
685     my $leave_name      = $options->{leave_name} || undef;
686
687     my @fields = $c->fields or next;
688
689     if ( $c->type eq PRIMARY_KEY ) {
690         return 'PRIMARY KEY (' . $qf . join("$qf, $qf", @fields). $qf . ')';
691     }
692     elsif ( $c->type eq UNIQUE ) {
693         return
694         'UNIQUE '. 
695             (defined $c->name ? $qf.truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH ).$qf.' ' : '').
696             '(' . $qf . join("$qf, $qf", @fields). $qf . ')';
697     }
698     elsif ( $c->type eq FOREIGN_KEY ) {
699         #
700         # Make sure FK field is indexed or MySQL complains.
701         #
702
703         my $table = $c->table;
704         my $c_name = truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH );
705
706         my $def = join(' ', 
707                        map { $_ || () } 
708                          'CONSTRAINT', 
709                          $qf . $c_name . $qf, 
710                          'FOREIGN KEY'
711                       );
712
713
714         $def .= ' ('.$qf . join( "$qf, $qf", @fields ) . $qf . ')';
715
716         $def .= ' REFERENCES ' . $qt . $c->reference_table . $qt;
717
718         my @rfields = map { $_ || () } $c->reference_fields;
719         unless ( @rfields ) {
720             my $rtable_name = $c->reference_table;
721             if ( my $ref_table = $table->schema->get_table( $rtable_name ) ) {
722                 push @rfields, $ref_table->primary_key;
723             }
724             else {
725                 warn "Can't find reference table '$rtable_name' " .
726                     "in schema\n" if $options->{show_warnings};
727             }
728         }
729
730         if ( @rfields ) {
731             $def .= ' (' . $qf . join( "$qf, $qf", @rfields ) . $qf . ')';
732         }
733         else {
734             warn "FK constraint on " . $table->name . '.' .
735                 join('', @fields) . " has no reference fields\n" 
736                 if $options->{show_warnings};
737         }
738
739         if ( $c->match_type ) {
740             $def .= ' MATCH ' . 
741                 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
742         }
743
744         if ( $c->on_delete ) {
745             $def .= ' ON DELETE '.join( ' ', $c->on_delete );
746         }
747
748         if ( $c->on_update ) {
749             $def .= ' ON UPDATE '.join( ' ', $c->on_update );
750         }
751         return $def;
752     }
753
754     return undef;
755 }
756
757 sub alter_table
758 {
759     my ($to_table, $options) = @_;
760
761     my $qt = $options->{quote_table_names} || '';
762
763     my $table_options = generate_table_options($to_table, $options) || '';
764     my $out = sprintf('ALTER TABLE %s%s',
765                       $qt . $to_table->name . $qt,
766                       $table_options);
767
768     return $out;
769 }
770
771 sub rename_field { alter_field(@_) }
772 sub alter_field
773 {
774     my ($from_field, $to_field, $options) = @_;
775
776     my $qf = $options->{quote_field_names} || '';
777     my $qt = $options->{quote_table_names} || '';
778
779     my $out = sprintf('ALTER TABLE %s CHANGE COLUMN %s %s',
780                       $qt . $to_field->table->name . $qt,
781                       $qf . $from_field->name . $qf,
782                       create_field($to_field, $options));
783
784     return $out;
785 }
786
787 sub add_field
788 {
789     my ($new_field, $options) = @_;
790
791     my $qt = $options->{quote_table_names} || '';
792
793     my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
794                       $qt . $new_field->table->name . $qt,
795                       create_field($new_field, $options));
796
797     return $out;
798
799 }
800
801 sub drop_field
802
803     my ($old_field, $options) = @_;
804
805     my $qf = $options->{quote_field_names} || '';
806     my $qt = $options->{quote_table_names} || '';
807     
808     my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
809                       $qt . $old_field->table->name . $qt,
810                       $qf . $old_field->name . $qf);
811
812     return $out;
813     
814 }
815
816 sub batch_alter_table {
817   my ($table, $diff_hash, $options) = @_;
818
819   # InnoDB has an issue with dropping and re-adding a FK constraint under the 
820   # name in a single alter statment, see: http://bugs.mysql.com/bug.php?id=13741
821   #
822   # We have to work round this.
823
824   my %fks_to_alter;
825   my %fks_to_drop = map {
826     $_->type eq FOREIGN_KEY 
827               ? ( $_->name => $_ ) 
828               : ( )
829   } @{$diff_hash->{alter_drop_constraint} };
830
831   my %fks_to_create = map {
832     if ( $_->type eq FOREIGN_KEY) {
833       $fks_to_alter{$_->name} = $fks_to_drop{$_->name} if $fks_to_drop{$_->name};
834       ( $_->name => $_ );
835     } else { ( ) }
836   } @{$diff_hash->{alter_create_constraint} };
837
838   my @drop_stmt;
839   if (scalar keys %fks_to_alter) {
840     $diff_hash->{alter_drop_constraint} = [
841       grep { !$fks_to_alter{$_->name} } @{ $diff_hash->{alter_drop_constraint} }
842     ];
843
844     @drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options);
845
846   }
847
848   my @stmts = map {
849     if (@{ $diff_hash->{$_} || [] }) {
850       my $meth = __PACKAGE__->can($_) or die __PACKAGE__ . " cant $_";
851       map { $meth->( (ref $_ eq 'ARRAY' ? @$_ : $_), $options ) } @{ $diff_hash->{$_} }
852     } else { () }
853   } qw/rename_table
854        alter_drop_constraint
855        alter_drop_index
856        drop_field
857        add_field
858        alter_field
859        rename_field
860        alter_create_index
861        alter_create_constraint
862        alter_table/;
863
864   # rename_table makes things a bit more complex
865   my $renamed_from = "";
866   $renamed_from = $diff_hash->{rename_table}[0][0]->name
867     if $diff_hash->{rename_table} && @{$diff_hash->{rename_table}};
868
869   return unless @stmts;
870   # Just zero or one stmts. return now
871   return (@drop_stmt,@stmts) unless @stmts > 1;
872
873   # Now strip off the 'ALTER TABLE xyz' of all but the first one
874
875   my $qt = $options->{quote_table_names} || '';
876   my $table_name = $qt . $table->name . $qt;
877
878
879   my $re = $renamed_from 
880          ? qr/^ALTER TABLE (?:\Q$table_name\E|\Q$qt$renamed_from$qt\E) /
881             : qr/^ALTER TABLE \Q$table_name\E /;
882
883   my $first = shift  @stmts;
884   my ($alter_table) = $first =~ /($re)/;
885
886   my $padd = " " x length($alter_table);
887
888   return @drop_stmt, join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts);
889
890 }
891
892 sub drop_table {
893   my ($table, $options) = @_;
894
895     my $qt = $options->{quote_table_names} || '';
896
897   # Drop (foreign key) constraints so table drops cleanly
898   my @sql = batch_alter_table($table, { alter_drop_constraint => [ grep { $_->type eq 'FOREIGN KEY' } $table->get_constraints ] }, $options);
899
900   return (@sql, "DROP TABLE $qt$table$qt");
901 #  return join("\n", @sql, "DROP TABLE $qt$table$qt");
902
903 }
904
905 sub rename_table {
906   my ($old_table, $new_table, $options) = @_;
907
908   my $qt = $options->{quote_table_names} || '';
909
910   return "ALTER TABLE $qt$old_table$qt RENAME TO $qt$new_table$qt";
911 }
912
913 sub next_unused_name {
914   my $name       = shift || '';
915   if ( !defined($used_names{$name}) ) {
916     $used_names{$name} = $name;
917     return $name;
918   }
919
920   my $i = 1;
921   while ( defined($used_names{$name . '_' . $i}) ) {
922     ++$i;
923   }
924   $name .= '_' . $i;
925   $used_names{$name} = $name;
926   return $name;
927 }
928
929 1;
930
931 # -------------------------------------------------------------------
932
933 =pod
934
935 =head1 SEE ALSO
936
937 SQL::Translator, http://www.mysql.com/.
938
939 =head1 AUTHORS
940
941 darren chamberlain E<lt>darren@cpan.orgE<gt>,
942 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
943
944 =cut