package SQL::Translator::Producer::MySQL;
-# -------------------------------------------------------------------
-# $Id: MySQL.pm,v 1.54 2007-11-10 03:36:43 mwz444 Exp $
-# -------------------------------------------------------------------
-# Copyright (C) 2002-4 SQLFairy Authors
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; version 2.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
-# 02111-1307 USA
-# -------------------------------------------------------------------
-
=head1 NAME
SQL::Translator::Producer::MySQL - MySQL-specific producer for SQL::Translator
between MySQL versions 3 and 4 ("SET foreign_key_checks," character sets
for fields, etc.).
+=head1 ARGUMENTS
+
+This producer takes a single optional producer_arg C<mysql_version>, which
+provides the desired version for the target database. By default MySQL v3 is
+assumed, and statements pertaining to any features introduced in later versions
+(e.g. CREATE VIEW) are not produced.
+
+Valid version specifiers for C<mysql_version> are listed L<here|SQL::Translator::Utils/parse_mysql_version>
+
=head2 Table Types
Normally the tables will be created without any explicit table type given and
automatically set for tables involved in foreign key constraints if it is
not already set explicitly. See L<"Table Types">.
-Please note that the C<ENGINE> option is the prefered method of specifying
+Please note that the C<ENGINE> option is the preferred method of specifying
the MySQL storage engine to use, but this method still works for backwards
-compatability.
+compatibility.
=item B<table.mysql_charset>, B<table.mysql_collate>
use strict;
use warnings;
use vars qw[ $VERSION $DEBUG %used_names ];
-$VERSION = sprintf "%d.%02d", q$Revision: 1.54 $ =~ /(\d+)\.(\d+)/;
+$VERSION = '1.59';
$DEBUG = 0 unless defined $DEBUG;
+# Maximum length for most identifiers is 64, according to:
+# http://dev.mysql.com/doc/refman/4.1/en/identifiers.html
+# http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
+my $DEFAULT_MAX_ID_LENGTH = 64;
+
use Data::Dumper;
use SQL::Translator::Schema::Constants;
-use SQL::Translator::Utils qw(debug header_comment);
+use SQL::Translator::Utils qw(debug header_comment
+ truncate_id_uniquely parse_mysql_version);
#
# Use only lowercase for the keys (e.g. "long" and not "LONG")
'long integer' => 'integer',
'text' => 'text',
'datetime' => 'datetime',
+
+ #
+ # PostgreSQL types
+ #
+ bytea => 'BLOB',
);
+#
+# Column types that do not support lenth attribute
+#
+my @no_length_attr = qw/
+ date time timestamp datetime year
+ /;
+
sub preprocess_schema {
my ($schema) = @_;
# extra->{mysql_table_type} used to be the type. It belongs in options, so
# move it if we find it. Return Engine type if found in extra or options
- my $mysql_table_type_to_options = sub {
- my ($table) = @_;
+ # Similarly for mysql_charset and mysql_collate
+ my $extra_to_options = sub {
+ my ($table, $extra_name, $opt_name) = @_;
my $extra = $table->extra;
- my $extra_type = delete $extra->{mysql_table_type};
+ my $extra_type = delete $extra->{$extra_name};
# Now just to find if there is already an Engine or Type option...
# and lets normalize it to ENGINE since:
# to remove options.
my $options = ( $table->{options} ||= []);
+ # If multiple option names, normalize to the first one
+ if (ref $opt_name) {
+ OPT_NAME: for ( @$opt_name[1..$#$opt_name] ) {
+ for my $idx ( 0..$#{$options} ) {
+ my ($key, $value) = %{ $options->[$idx] };
+
+ if (uc $key eq $_) {
+ $options->[$idx] = { $opt_name->[0] => $value };
+ last OPT_NAME;
+ }
+ }
+ }
+ $opt_name = $opt_name->[0];
+
+ }
+
+
# This assumes that there isn't both a Type and an Engine option.
+ OPTION:
for my $idx ( 0..$#{$options} ) {
my ($key, $value) = %{ $options->[$idx] };
- next unless uc $key eq 'ENGINE' || uc $key eq 'TYPE';
-
- # if the extra.mysql_table_type is given, use that
+ next unless uc $key eq $opt_name;
+
+ # make sure case is right on option name
delete $options->[$idx]{$key};
- return $options->[$idx]{ENGINE} = $value || $extra_type;
+ return $options->[$idx]{$opt_name} = $value || $extra_type;
}
if ($extra_type) {
- push @$options, { ENGINE => $extra_type };
+ push @$options, { $opt_name => $extra_type };
return $extra_type;
}
# constraints. We do this first as we need InnoDB at both ends.
#
foreach my $table ( $schema->get_tables ) {
-
- $mysql_table_type_to_options->($table);
+
+ $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE'] );
+ $extra_to_options->($table, 'mysql_charset', 'CHARACTER SET' );
+ $extra_to_options->($table, 'mysql_collate', 'COLLATE' );
foreach my $c ( $table->get_constraints ) {
next unless $c->type eq FOREIGN_KEY;
for my $meth (qw/table reference_table/) {
my $table = $schema->get_table($c->$meth) || next;
- next if $mysql_table_type_to_options->($table);
+ # This normalizes the types to ENGINE and returns the value if its there
+ next if $extra_to_options->($table, 'mysql_table_type', ['ENGINE', 'TYPE']);
$table->options( { 'ENGINE' => 'InnoDB' } );
}
} # foreach constraints
+ my %map = ( mysql_collate => 'collate', mysql_charset => 'character set');
foreach my $f ( $table->get_fields ) {
+ my $extra = $f->extra;
+ for (keys %map) {
+ $extra->{$map{$_}} = delete $extra->{$_} if exists $extra->{$_};
+ }
+
my @size = $f->size;
if ( !$size[0] && $f->data_type =~ /char$/ ) {
$f->size( (255) );
my $add_drop_table = $translator->add_drop_table;
my $schema = $translator->schema;
my $show_warnings = $translator->show_warnings || 0;
+ my $producer_args = $translator->producer_args;
+ my $mysql_version = parse_mysql_version ($producer_args->{mysql_version}, 'perl') || 0;
+ my $max_id_length = $producer_args->{mysql_max_id_length} || $DEFAULT_MAX_ID_LENGTH;
- my ($qt, $qf) = ('','');
+ my ($qt, $qf, $qc) = ('','', '');
$qt = '`' if $translator->quote_table_names;
$qf = '`' if $translator->quote_field_names;
debug("PKG: Beginning production\n");
%used_names = ();
- my $create;
+ my $create = '';
$create .= header_comment unless ($no_comments);
# \todo Don't set if MySQL 3.x is set on command line
- $create .= "SET foreign_key_checks=0;\n\n";
+ my @create = "SET foreign_key_checks=0";
preprocess_schema($schema);
show_warnings => $show_warnings,
no_comments => $no_comments,
quote_table_names => $qt,
- quote_field_names => $qf
+ quote_field_names => $qf,
+ max_id_length => $max_id_length,
+ mysql_version => $mysql_version
+ });
+ }
+
+ if ($mysql_version >= 5.000001) {
+ for my $view ( $schema->get_views ) {
+ push @table_defs, create_view($view,
+ { add_replace_view => $add_drop_table,
+ show_warnings => $show_warnings,
+ no_comments => $no_comments,
+ quote_table_names => $qt,
+ quote_field_names => $qf,
+ max_id_length => $max_id_length,
+ mysql_version => $mysql_version
});
+ }
+ }
+
+ if ($mysql_version >= 5.000002) {
+ for my $trigger ( $schema->get_triggers ) {
+ push @table_defs, create_trigger($trigger,
+ { add_drop_trigger => $add_drop_table,
+ show_warnings => $show_warnings,
+ no_comments => $no_comments,
+ quote_table_names => $qt,
+ quote_field_names => $qf,
+ max_id_length => $max_id_length,
+ mysql_version => $mysql_version
+ });
+ }
}
+
# print "@table_defs\n";
- push @table_defs, "SET foreign_key_checks=1;\n\n";
+ push @table_defs, "SET foreign_key_checks=1";
+
+ return wantarray ? ($create ? $create : (), @create, @table_defs) : ($create . join('', map { $_ ? "$_;\n\n" : () } (@create, @table_defs)));
+}
+
+sub create_trigger {
+ my ($trigger, $options) = @_;
+ my $qt = $options->{quote_table_names} || '';
+ my $qf = $options->{quote_field_names} || '';
- return wantarray ? ($create, @table_defs) : $create . join ('', @table_defs);
+ my $trigger_name = $trigger->name;
+ debug("PKG: Looking at trigger '${trigger_name}'\n");
+
+ my @statements;
+
+ my $events = $trigger->database_events;
+ for my $event ( @$events ) {
+ my $name = $trigger_name;
+ if (@$events > 1) {
+ $name .= "_$event";
+
+ warn "Multiple database events supplied for trigger '${trigger_name}', ",
+ "creating trigger '${name}' for the '${event}' event\n"
+ if $options->{show_warnings};
+ }
+
+ my $action = $trigger->action;
+ $action .= ";" unless $action =~ /;\s*\z/;
+
+ push @statements, "DROP TRIGGER IF EXISTS ${qt}${name}${qt}" if $options->{add_drop_trigger};
+ push @statements, sprintf(
+ "CREATE TRIGGER ${qt}%s${qt} %s %s ON ${qt}%s${qt}\n FOR EACH ROW BEGIN %s END",
+ $name, $trigger->perform_action_when, $event, $trigger->on_table, $action,
+ );
+
+ }
+ # Tack the comment onto the first statement
+ $statements[0] = "--\n-- Trigger ${qt}${trigger_name}${qt}\n--\n" . $statements[0] unless $options->{no_comments};
+ return @statements;
+}
+
+sub create_view {
+ my ($view, $options) = @_;
+ my $qt = $options->{quote_table_names} || '';
+ my $qf = $options->{quote_field_names} || '';
+
+ my $view_name = $view->name;
+ debug("PKG: Looking at view '${view_name}'\n");
+
+ # Header. Should this look like what mysqldump produces?
+ my $create = '';
+ $create .= "--\n-- View: ${qt}${view_name}${qt}\n--\n" unless $options->{no_comments};
+ $create .= 'CREATE';
+ $create .= ' OR REPLACE' if $options->{add_replace_view};
+ $create .= "\n";
+
+ my $extra = $view->extra;
+ # ALGORITHM
+ if( exists($extra->{mysql_algorithm}) && defined(my $algorithm = $extra->{mysql_algorithm}) ){
+ $create .= " ALGORITHM = ${algorithm}\n" if $algorithm =~ /(?:UNDEFINED|MERGE|TEMPTABLE)/i;
+ }
+ # DEFINER
+ if( exists($extra->{mysql_definer}) && defined(my $user = $extra->{mysql_definer}) ){
+ $create .= " DEFINER = ${user}\n";
+ }
+ # SECURITY
+ if( exists($extra->{mysql_security}) && defined(my $security = $extra->{mysql_security}) ){
+ $create .= " SQL SECURITY ${security}\n" if $security =~ /(?:DEFINER|INVOKER)/i;
+ }
+
+ #Header, cont.
+ $create .= " VIEW ${qt}${view_name}${qt}";
+
+ if( my @fields = $view->fields ){
+ my $list = join ', ', map { "${qf}${_}${qf}"} @fields;
+ $create .= " ( ${list} )";
+ }
+ if( my $sql = $view->sql ){
+ # do not wrap parenthesis around the selector, mysql doesn't like this
+ # http://bugs.mysql.com/bug.php?id=9198
+ $create .= " AS\n ${sql}\n";
+ }
+# $create .= "";
+ return $create;
}
sub create_table
my $qt = $options->{quote_table_names} || '';
my $qf = $options->{quote_field_names} || '';
- my $table_name = $table->name;
+ my $table_name = quote_table_name($table->name, $qt);
debug("PKG: Looking at table '$table_name'\n");
#
#
my $create = '';
my $drop;
- $create .= "--\n-- Table: $qt$table_name$qt\n--\n" unless $options->{no_comments};
- $drop = qq[DROP TABLE IF EXISTS $qt$table_name$qt;\n] if $options->{add_drop_table};
- $create .= "CREATE TABLE $qt$table_name$qt (\n";
+ $create .= "--\n-- Table: $table_name\n--\n" unless $options->{no_comments};
+ $drop = qq[DROP TABLE IF EXISTS $table_name] if $options->{add_drop_table};
+ $create .= "CREATE TABLE $table_name (\n";
#
# Fields
# Footer
#
$create .= "\n)";
- $create .= generate_table_options($table) || '';
- $create .= ";\n\n";
+ $create .= generate_table_options($table, $options) || '';
+# $create .= ";\n\n";
return $drop ? ($drop,$create) : $create;
}
+sub quote_table_name {
+ my ($table_name, $qt) = @_;
+
+ $table_name =~ s/\./$qt.$qt/g;
+
+ return "$qt$table_name$qt";
+}
+
sub generate_table_options
{
- my ($table) = @_;
+ my ($table, $options) = @_;
my $create;
my $table_type_defined = 0;
+ my $qf = $options->{quote_field_names} ||= '';
+ my $charset = $table->extra('mysql_charset');
+ my $collate = $table->extra('mysql_collate');
+ my $union = undef;
for my $t1_option_ref ( $table->options ) {
my($key, $value) = %{$t1_option_ref};
$table_type_defined = 1
if uc $key eq 'ENGINE' or uc $key eq 'TYPE';
+ if (uc $key eq 'CHARACTER SET') {
+ $charset = $value;
+ next;
+ } elsif (uc $key eq 'COLLATE') {
+ $collate = $value;
+ next;
+ } elsif (uc $key eq 'UNION') {
+ $union = "($qf". join("$qf, $qf", @$value) ."$qf)";
+ next;
+ }
$create .= " $key=$value";
}
my $mysql_table_type = $table->extra('mysql_table_type');
$create .= " ENGINE=$mysql_table_type"
if $mysql_table_type && !$table_type_defined;
- my $charset = $table->extra('mysql_charset');
- my $collate = $table->extra('mysql_collate');
my $comments = $table->comments;
$create .= " DEFAULT CHARACTER SET $charset" if $charset;
$create .= " COLLATE $collate" if $collate;
+ $create .= " UNION=$union" if $union;
$create .= qq[ comment='$comments'] if $comments;
return $create;
}
my $charset = $extra{'mysql_charset'};
my $collate = $extra{'mysql_collate'};
+ my $mysql_version = $options->{mysql_version} || 0;
#
# Oracle "number" type -- figure best MySQL type
#
}
#
# Convert a large Oracle varchar to "text"
+ # (not necessary as of 5.0.3 http://dev.mysql.com/doc/refman/5.0/en/char.html)
#
elsif ( $data_type =~ /char/i && $size[0] > 255 ) {
- $data_type = 'text';
- @size = ();
+ unless ($size[0] <= 65535 && $mysql_version >= 5.000003 ) {
+ $data_type = 'text';
+ @size = ();
+ }
}
elsif ( $data_type =~ /boolean/i ) {
- $data_type = 'enum';
- $commalist = "'0','1'";
+ if ($mysql_version >= 4) {
+ $data_type = 'boolean';
+ } else {
+ $data_type = 'enum';
+ $commalist = "'0','1'";
+ }
}
elsif ( exists $translate{ lc $data_type } ) {
$data_type = $translate{ lc $data_type };
$field_def .= " $data_type";
- if ( lc $data_type eq 'enum' ) {
+ if ( lc($data_type) eq 'enum' || lc($data_type) eq 'set') {
$field_def .= '(' . $commalist . ')';
- }
- elsif ( defined $size[0] && $size[0] > 0 ) {
+ }
+ elsif (
+ defined $size[0] && $size[0] > 0
+ &&
+ ! grep lc($data_type) eq $_, @no_length_attr
+ ) {
$field_def .= '(' . join( ', ', @size ) . ')';
}
# Null?
$field_def .= ' NOT NULL' unless $field->is_nullable;
- # Default? XXX Need better quoting!
- my $default = $field->default_value;
- if ( defined $default ) {
- if ( uc $default eq 'NULL') {
- $field_def .= ' DEFAULT NULL';
- } else {
- $field_def .= " DEFAULT '$default'";
- }
- }
+ # Default?
+ SQL::Translator::Producer->_apply_default_value(
+ $field,
+ \$field_def,
+ [
+ 'NULL' => \'NULL',
+ ],
+ );
if ( my $comments = $field->comments ) {
$field_def .= qq[ comment '$comments'];
sub create_index
{
- my ($index, $options) = @_;
+ my ( $index, $options ) = @_;
my $qf = $options->{quote_field_names} || '';
- return join( ' ',
- lc $index->type eq 'normal' ? 'INDEX' : $index->type . ' INDEX',
- $index->name,
- '(' . $qf . join( "$qf, $qf", $index->fields ) . $qf . ')'
- );
-
+ return join(
+ ' ',
+ map { $_ || () }
+ lc $index->type eq 'normal' ? 'INDEX' : $index->type . ' INDEX',
+ $index->name
+ ? $qf . truncate_id_uniquely(
+ $index->name,
+ $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH
+ ) . $qf
+ : '',
+ '(' . $qf . join( "$qf, $qf", $index->fields ) . $qf . ')'
+ );
}
sub alter_drop_index
my ($c, $options) = @_;
my $qt = $options->{quote_table_names} || '';
- my $qc = $options->{quote_constraint_names} || '';
+ my $qc = $options->{quote_field_names} || '';
my $out = sprintf('ALTER TABLE %s DROP %s %s',
$qt . $c->table->name . $qt,
my $qf = $options->{quote_field_names} || '';
my $qt = $options->{quote_table_names} || '';
- my $qc = $options->{quote_constraint_names} || '';
my $leave_name = $options->{leave_name} || undef;
my @fields = $c->fields or next;
elsif ( $c->type eq UNIQUE ) {
return
'UNIQUE '.
- (defined $c->name ? $qf.$c->name.$qf.' ' : '').
+ (defined $c->name ? $qf.truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH ).$qf.' ' : '').
'(' . $qf . join("$qf, $qf", @fields). $qf . ')';
}
elsif ( $c->type eq FOREIGN_KEY ) {
#
my $table = $c->table;
- my $c_name = $c->name;
+ my $c_name = truncate_id_uniquely( $c->name, $options->{max_id_length} || $DEFAULT_MAX_ID_LENGTH );
my $def = join(' ',
map { $_ || () }
'CONSTRAINT',
- $qc . $c_name . $qc,
+ $qf . $c_name . $qf,
'FOREIGN KEY'
);
my $qt = $options->{quote_table_names} || '';
- my $table_options = generate_table_options($to_table) || '';
+ my $table_options = generate_table_options($to_table, $options) || '';
my $out = sprintf('ALTER TABLE %s%s',
$qt . $to_table->name . $qt,
$table_options);
} else { ( ) }
} @{$diff_hash->{alter_create_constraint} };
- my $drop_stmt = '';
+ my @drop_stmt;
if (scalar keys %fks_to_alter) {
$diff_hash->{alter_drop_constraint} = [
grep { !$fks_to_alter{$_->name} } @{ $diff_hash->{alter_drop_constraint} }
];
- $drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options)
- . "\n";
+ @drop_stmt = batch_alter_table($table, { alter_drop_constraint => [ values %fks_to_alter ] }, $options);
}
return unless @stmts;
# Just zero or one stmts. return now
- return "$drop_stmt@stmts;" unless @stmts > 1;
+ return (@drop_stmt,@stmts) unless @stmts > 1;
# Now strip off the 'ALTER TABLE xyz' of all but the first one
my $padd = " " x length($alter_table);
- return $drop_stmt . join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts) . ';';
+ return @drop_stmt, join( ",\n", $first, map { s/$re//; $padd . $_ } @stmts);
}
# Drop (foreign key) constraints so table drops cleanly
my @sql = batch_alter_table($table, { alter_drop_constraint => [ grep { $_->type eq 'FOREIGN KEY' } $table->get_constraints ] }, $options);
- return join("\n", @sql, "DROP TABLE $qt$table$qt;");
+ return (@sql, "DROP TABLE $qt$table$qt");
+# return join("\n", @sql, "DROP TABLE $qt$table$qt");
}
=head1 AUTHORS
darren chamberlain E<lt>darren@cpan.orgE<gt>,
-Ken Y. Clark E<lt>kclark@cpan.orgE<gt>.
+Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>.
=cut