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