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