Release commit for 1.62
[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.62';
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 : "CONSTRAINT"),
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;
747
748     if ( $c->type eq PRIMARY_KEY ) {
749         return unless @fields;
750         return 'PRIMARY KEY (' . join(", ", map { $generator->quote($_) } @fields) . ')';
751     }
752     elsif ( $c->type eq UNIQUE ) {
753         return unless @fields;
754         return sprintf 'UNIQUE %s(%s)',
755           ((defined $c->name && $c->name)
756             ? $generator->quote(
757                   truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH ),
758               ) . ' '
759             : ''
760           ),
761           ( join ', ', map { $generator->quote($_) } @fields ),
762         ;
763     }
764     elsif ( $c->type eq FOREIGN_KEY ) {
765         return unless @fields;
766         #
767         # Make sure FK field is indexed or MySQL complains.
768         #
769
770         my $table = $c->table;
771         my $c_name = truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH );
772
773         my $def = join(' ',
774                          'CONSTRAINT',
775                          ($c_name ? $generator->quote($c_name) : () ),
776                          'FOREIGN KEY'
777                       );
778
779
780         $def .= ' ('. join( ', ', map { $generator->quote($_) } @fields ) . ')';
781
782         $def .= ' REFERENCES ' . $reference_table_name;
783
784         my @rfields = map { $_ || () } $c->reference_fields;
785         unless ( @rfields ) {
786             my $rtable_name = $c->reference_table;
787             if ( my $ref_table = $table->schema->get_table( $rtable_name ) ) {
788                 push @rfields, $ref_table->primary_key;
789             }
790             else {
791                 warn "Can't find reference table '$rtable_name' " .
792                     "in schema\n" if $options->{show_warnings};
793             }
794         }
795
796         if ( @rfields ) {
797             $def .= ' (' . join( ', ', map { $generator->quote($_) } @rfields ) . ')';
798         }
799         else {
800             warn "FK constraint on " . $table->name . '.' .
801                 join('', @fields) . " has no reference fields\n"
802                 if $options->{show_warnings};
803         }
804
805         if ( $c->match_type ) {
806             $def .= ' MATCH ' .
807                 ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL';
808         }
809
810         if ( $c->on_delete ) {
811             $def .= ' ON DELETE '. $c->on_delete;
812         }
813
814         if ( $c->on_update ) {
815             $def .= ' ON UPDATE '. $c->on_update;
816         }
817         return $def;
818     }
819     elsif ( $c->type eq CHECK_C ) {
820         my $table = $c->table;
821         my $c_name = truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH );
822
823         my $def = join(' ',
824                          'CONSTRAINT',
825                          ($c_name ? $generator->quote($c_name) : () ),
826                          'CHECK'
827                       );
828
829
830         $def .= ' ('. $c->expression . ')';
831         return $def;
832     }
833
834     return undef;
835 }
836
837 sub alter_table
838 {
839     my ($to_table, $options) = @_;
840
841     my $table_options = generate_table_options($to_table, $options) || '';
842     my $table_name = _generator($options)->quote($to_table->name);
843     my $out = sprintf('ALTER TABLE %s%s',
844                       $table_name,
845                       $table_options);
846
847     return $out;
848 }
849
850 sub rename_field { alter_field(@_) }
851 sub alter_field
852 {
853     my ($from_field, $to_field, $options) = @_;
854
855     my $generator  = _generator($options);
856     my $table_name = $generator->quote($to_field->table->name);
857
858     my $out = sprintf('ALTER TABLE %s CHANGE COLUMN %s %s',
859                       $table_name,
860                       $generator->quote($from_field->name),
861                       create_field($to_field, $options));
862
863     return $out;
864 }
865
866 sub add_field
867 {
868     my ($new_field, $options) = @_;
869
870     my $table_name = _generator($options)->quote($new_field->table->name);
871
872     my $out = sprintf('ALTER TABLE %s ADD COLUMN %s',
873                       $table_name,
874                       create_field($new_field, $options));
875
876     return $out;
877
878 }
879
880 sub drop_field
881 {
882     my ($old_field, $options) = @_;
883
884     my $generator  = _generator($options);
885     my $table_name = $generator->quote($old_field->table->name);
886
887     my $out = sprintf('ALTER TABLE %s DROP COLUMN %s',
888                       $table_name,
889                       $generator->quote($old_field->name));
890
891     return $out;
892
893 }
894
895 sub batch_alter_table {
896   my ($table, $diff_hash, $options) = @_;
897
898   # InnoDB has an issue with dropping and re-adding a FK constraint under the
899   # name in a single alter statement, see: http://bugs.mysql.com/bug.php?id=13741
900   #
901   # We have to work round this.
902
903   my %fks_to_alter;
904   my %fks_to_drop = map {
905     $_->type eq FOREIGN_KEY
906               ? ( $_->name => $_ )
907               : ( )
908   } @{$diff_hash->{alter_drop_constraint} };
909
910   my %fks_to_create = map {
911     if ( $_->type eq FOREIGN_KEY) {
912       $fks_to_alter{$_->name} = $fks_to_drop{$_->name} if $fks_to_drop{$_->name};
913       ( $_->name => $_ );
914     } else { ( ) }
915   } @{$diff_hash->{alter_create_constraint} };
916
917   my @drop_stmt;
918   if (scalar keys %fks_to_alter) {
919     $diff_hash->{alter_drop_constraint} = [
920       grep { !$fks_to_alter{$_->name} } @{ $diff_hash->{alter_drop_constraint} }
921     ];
922
923     @drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options);
924
925   }
926
927   my @stmts = batch_alter_table_statements($diff_hash, $options);
928
929   #quote
930   my $generator = _generator($options);
931
932   # rename_table makes things a bit more complex
933   my $renamed_from = "";
934   $renamed_from = $generator->quote($diff_hash->{rename_table}[0][0]->name)
935     if $diff_hash->{rename_table} && @{$diff_hash->{rename_table}};
936
937   return unless @stmts;
938   # Just zero or one stmts. return now
939   return (@drop_stmt,@stmts) unless @stmts > 1;
940
941   # Now strip off the 'ALTER TABLE xyz' of all but the first one
942
943   my $table_name = $generator->quote($table->name);
944
945   my $re = $renamed_from
946          ? qr/^ALTER TABLE (?:\Q$table_name\E|\Q$renamed_from\E) /
947             : qr/^ALTER TABLE \Q$table_name\E /;
948
949   my $first = shift  @stmts;
950   my ($alter_table) = $first =~ /($re)/;
951
952   my $padd = " " x length($alter_table);
953
954   return @drop_stmt, join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts);
955
956 }
957
958 sub drop_table {
959   my ($table, $options) = @_;
960
961   return (
962     # Drop (foreign key) constraints so table drops cleanly
963     batch_alter_table(
964       $table, { alter_drop_constraint => [ grep { $_->type eq 'FOREIGN KEY' } $table->get_constraints ] }, $options
965     ),
966     'DROP TABLE ' . _generator($options)->quote($table),
967   );
968 }
969
970 sub rename_table {
971   my ($old_table, $new_table, $options) = @_;
972
973   my $generator      = _generator($options);
974   my $old_table_name = $generator->quote($old_table);
975   my $new_table_name = $generator->quote($new_table);
976
977   return "ALTER TABLE $old_table_name RENAME TO $new_table_name";
978 }
979
980 sub next_unused_name {
981   my $name       = shift || '';
982   if ( !defined($used_names{$name}) ) {
983     $used_names{$name} = $name;
984     return $name;
985   }
986
987   my $i = 1;
988   while ( defined($used_names{$name . '_' . $i}) ) {
989     ++$i;
990   }
991   $name .= '_' . $i;
992   $used_names{$name} = $name;
993   return $name;
994 }
995
996 1;
997
998 =pod
999
1000 =head1 SEE ALSO
1001
1002 SQL::Translator, http://www.mysql.com/.
1003
1004 =head1 AUTHORS
1005
1006 darren chamberlain E<lt>darren@cpan.orgE<gt>,
1007 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
1008
1009 =cut