From: Chris Hilton Date: Mon, 27 Jun 2005 22:09:42 +0000 (+0000) Subject: A whole lot of changes, but major additions include adding diffs for table options... X-Git-Tag: v0.11008~519 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=d71512a62e2fc09f6ee6eaea9320fafb4ea7585b;p=dbsrgits%2FSQL-Translator.git A whole lot of changes, but major additions include adding diffs for table options and constraints Many smaller changes for various quirks of SQL Server and MySQL --- diff --git a/bin/sqlt-diff b/bin/sqlt-diff index d354801..0ab4cba 100755 --- a/bin/sqlt-diff +++ b/bin/sqlt-diff @@ -2,7 +2,7 @@ # vim: set ft=perl: # ------------------------------------------------------------------- -# $Id: sqlt-diff,v 1.6 2004-02-27 18:26:38 kycl4rk Exp $ +# $Id: sqlt-diff,v 1.7 2005-06-27 22:09:42 duality72 Exp $ # ------------------------------------------------------------------- # Copyright (C) 2002-4 The SQLFairy Authors # @@ -97,7 +97,7 @@ use SQL::Translator; use SQL::Translator::Schema::Constants; use vars qw( $VERSION ); -$VERSION = sprintf "%d.%02d", q$Revision: 1.6 $ =~ /(\d+)\.(\d+)/; +$VERSION = sprintf "%d.%02d", q$Revision: 1.7 $ =~ /(\d+)\.(\d+)/; my ( @input, $list, $help, $debug ); for my $arg ( @ARGV ) { @@ -162,27 +162,85 @@ for my $in ( @input ) { } $i--; } +my $case_insensitive = $target_db =~ /SQLServer/; my $s1_name = $source_schema->name; my $s2_name = $target_schema->name; -my ( @new_tables, @diffs ); +my ( @new_tables, @diffs , @diffs_at_end); for my $t1 ( $source_schema->get_tables ) { my $t1_name = $t1->name; - my $t2 = $target_schema->get_table( $t1_name ); + my $t2 = $target_schema->get_table( $t1_name, $case_insensitive ); warn "TABLE '$s1_name.$t1_name'\n" if $debug; unless ( $t2 ) { warn "Couldn't find table '$s1_name.$t1_name' in '$s2_name'\n" if $debug; + if ( $target_db =~ /SQLServer/ ) { + for my $constraint ( $t1->get_constraints ) { + next if $constraint->type eq PRIMARY_KEY; + push @diffs_at_end, "ALTER TABLE $t1_name ADD ". + constraint_to_string($constraint, $source_schema).";"; + $t1->drop_constraint($constraint); + } + } push @new_tables, $t1; next; } - + + # Go through our options + my $options_different = 0; + my %checkedOptions; +OPTION: + for my $t1_option_ref ( $t1->options ) { + my($key1, $value1) = %{$t1_option_ref}; + for my $t2_option_ref ( $t2->options ) { + my($key2, $value2) = %{$t2_option_ref}; + if ( $key1 eq $key2 ) { + if ( defined $value1 != defined $value2 ) { + $options_different = 1; + last OPTION; + } + if ( defined $value1 && $value1 ne $value2 ) { + $options_different = 1; + last OPTION; + } + $checkedOptions{$key1} = 1; + next OPTION; + } + } + $options_different = 1; + last OPTION; + } + # Go through the other table's options + unless ( $options_different ) { + for my $t2_option_ref ( $t2->options ) { + my($key, $value) = %{$t2_option_ref}; + next if $checkedOptions{$key}; + $options_different = 1; + last; + } + } + # If there's a difference, just re-set all the options + my @diffs_table_options; + if ( $options_different ) { + my @options = (); + foreach my $option_ref ( $t1->options ) { + my($key, $value) = %{$option_ref}; + push(@options, defined $value ? "$key=$value" : $key); + } + my $options = join(' ', @options); + @diffs_table_options = ("ALTER TABLE $t1_name $options;"); + } + my $t2_name = $t2->name; + my(@diffs_table_adds, @diffs_table_changes); for my $t1_field ( $t1->get_fields ) { my $f1_type = $t1_field->data_type; my $f1_size = $t1_field->size; my $f1_name = $t1_field->name; + my $f1_nullable = $t1_field->is_nullable; + my $f1_default = $t1_field->default_value; + my $f1_auto_inc = $t1_field->is_auto_increment; my $t2_field = $t2->get_field( $f1_name ); my $f1_full_name = "$s1_name.$t1_name.$t1_name"; warn "FIELD '$f1_full_name'\n" if $debug; @@ -192,96 +250,166 @@ for my $t1 ( $source_schema->get_tables ) { unless ( $t2_field ) { warn "Couldn't find field '$f2_full_name' in '$t2_name'\n" if $debug; - push @diffs, sprintf( "ALTER TABLE %s ADD %s %s%s;", + my $temp_default_value = 0; + if ( $target_db =~ /SQLServer/ && !$f1_nullable && !defined $f1_default ) { + # SQL Server doesn't allow adding non-nullable, non-default columns + # so we add it with a default value, then remove the default value + $temp_default_value = 1; + my(@numeric_types) = qw(decimal numeric float real int bigint smallint tinyint); + $f1_default = grep($_ eq $f1_type, @numeric_types) ? 0 : ''; + } + push @diffs_table_adds, sprintf( "ALTER TABLE %s ADD %s %s%s%s%s%s;", $t1_name, $f1_name, $f1_type, - $f1_size ? "($f1_size)" : '' + ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '', + $f1_nullable ? '' : ' NOT NULL', + !defined $f1_default ? '' + : uc $f1_default eq 'NULL' ? ' DEFAULT NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP' + : " DEFAULT '$f1_default'", + $f1_auto_inc ? ' AUTO_INCREMENT' : '', ); + if ( $temp_default_value ) { + undef $f1_default; + push @diffs_table_adds, sprintf( "ALTER TABLE %s %s %s%s %s%s%s%s%s;", + $t1_name, $target_db =~ /SQLServer/ ? "ALTER COLUMN" : "CHANGE", + $f1_name, $target_db =~ /MySQL/ ? " $f1_name" : '', + $f1_type, ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '', + $f1_nullable ? '' : ' NOT NULL', + !defined $f1_default || $target_db =~ /SQLServer/ ? '' + : uc $f1_default eq 'NULL' ? ' DEFAULT NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP' + : " DEFAULT '$f1_default'", + $f1_auto_inc ? ' AUTO_INCREMENT' : '', + ); + } next; } my $f2_type = $t2_field->data_type; - my $f2_size = $t2_field->size; - - if ( lc $f1_type ne lc $f2_type || - ( defined $f1_size && ( $f1_size ne $f2_size ) ) - ) { - push @diffs, sprintf( "ALTER TABLE %s CHANGE %s %s%s;", - $t1_name, $f1_name, $f1_type, - $f1_size ? "($f1_size)" : '' + my $f2_size = $t2_field->size || ''; + my $f2_nullable = $t2_field->is_nullable; + my $f2_default = $t2_field->default_value; + my $f2_auto_inc = $t2_field->is_auto_increment; + + if ( !$t1_field->equals($t2_field, $case_insensitive) ) { + # SQLServer timstamp fields can't be altered, so we drop and add instead + if ( $target_db =~ /SQLServer/ && $f2_type eq "timestamp" ) { + push @diffs_table_changes, "ALTER TABLE $t1_name DROP COLUMN $f1_name;"; + push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD %s %s%s%s%s%s;", + $t1_name, $f1_name, $f1_type, + ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '', + $f1_nullable ? '' : ' NOT NULL', + !defined $f1_default ? '' + : uc $f1_default eq 'NULL' ? ' DEFAULT NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP' + : " DEFAULT '$f1_default'", + $f1_auto_inc ? ' AUTO_INCREMENT' : '', + ); + next; + } + + push @diffs_table_changes, sprintf( "ALTER TABLE %s %s %s%s %s%s%s%s%s;", + $t1_name, $target_db =~ /SQLServer/ ? "ALTER COLUMN" : "CHANGE", + $f1_name, $target_db =~ /MySQL/ ? " $f1_name" : '', + $f1_type, ($f1_size && $f1_type !~ /(blob|text)$/) ? "($f1_size)" : '', + $f1_nullable ? '' : ' NOT NULL', + !defined $f1_default || $target_db =~ /SQLServer/ ? '' + : uc $f1_default eq 'NULL' ? ' DEFAULT NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? ' DEFAULT CURRENT_TIMESTAMP' + : " DEFAULT '$f1_default'", + $f1_auto_inc ? ' AUTO_INCREMENT' : '', ); - } - } - - my ( %t1_indices, %t2_indices ); - for my $rec ( [ $t1, \%t1_indices ], [ $t2, \%t2_indices ] ) { - my ( $table, $indices ) = @$rec; - for my $index ( $table->get_indices ) { - my $name = $index->name; - my $type = $index->type; - my $fields = join( ',', sort $index->fields ); - - $indices->{'type'}{ $type }{ $fields } = $name; - - if ( $name ) { - $indices->{'name'}{ $name } = { - type => $type, - fields => $fields, - }; - } - } - } - - for my $type ( keys %{ $t2_indices{'type'} } ) { - while ( my ($fields, $iname) = each %{$t2_indices{'type'}{ $type } } ) { - if ( $iname ) { - if ( my $i1 = $t1_indices{'name'}{ $iname } ) { - my $i1_type = $i1->{'type'}; - my $i1_fields = $i1->{'fields'}; - if ( $i1_type eq $type && $i1_fields eq $fields ) { - next; - } - } + if ( defined $f1_default && $target_db =~ /SQLServer/ ) { + # Adding a column with a default value for SQL Server means adding a + # constraint and setting existing NULLs to the default value + push @diffs_table_changes, sprintf( "ALTER TABLE %s ADD CONSTRAINT DF_%s_%s %s FOR %s;", + $t1_name, $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'DEFAULT NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'DEFAULT CURRENT_TIMESTAMP' + : "DEFAULT '$f1_default'", $f1_name, + ); + push @diffs_table_changes, sprintf( "UPDATE %s SET %s = %s WHERE %s IS NULL;", + $t1_name, $f1_name, uc $f1_default eq 'NULL' ? 'NULL' + : uc $f1_default eq 'CURRENT_TIMESTAMP' ? 'CURRENT_TIMESTAMP' + : "'$f1_default'", $f1_name, + ); } - elsif ( my $i1 = $t1_indices{'type'}{ $type }{ $fields } ) { - next; - } - - push @diffs, "DROP INDEX $iname on $t1_name;"; } } - - for my $type ( keys %{ $t1_indices{'type'} } ) { - while ( my ($fields, $iname) = each %{$t1_indices{'type'}{ $type } } ) { - if ( $iname ) { - if ( my $i2 = $t2_indices{'name'}{ $iname } ) { - my $i2_type = $i2->{'type'}; - my $i2_fields = $i2->{'fields'}; - if ( $i2_type eq $type && $i2_fields eq $fields ) { - next; - } - } - } - elsif ( my $i2 = $t2_indices{'type'}{ $type }{ $fields } ) { - next; - } - - push @diffs, sprintf( + + my(%table2_indices, @diffs_index_creates, @diffs_index_drops); + for my $i2 ( $t2->get_indices ) { + $table2_indices{$i2} = $i2; + } +INDEX: + for my $i1 ( $t1->get_indices ) { + for my $i2 ( keys %table2_indices ) { + $i2 = $table2_indices{$i2}; + if ( $i1->equals($i2, $case_insensitive) ) { + delete $table2_indices{$i2}; + next INDEX; + } + } + push @diffs_index_creates, sprintf( "CREATE %sINDEX%s ON %s (%s);", - $type eq NORMAL ? '' : "$type ", - $iname ? " $iname" : '', + $i1->type eq NORMAL ? '' : $i1->type." ", + $i1->name ? " ".$i1->name : '', $t1_name, - $fields, + join(",", $i1->fields), ); - } - } + } + for my $i2 ( keys %table2_indices ) { + $i2 = $table2_indices{$i2}; + $target_db =~ /SQLServer/ + ? push @diffs_index_drops, "DROP INDEX $t1_name.".$i2->name.";" + : push @diffs_index_drops, "DROP INDEX ".$i2->name." on $t1_name;"; + } + + my(%table2_constraints, @diffs_constraint_adds, @diffs_constraint_drops); + for my $c2 ( $t2->get_constraints ) { + $table2_constraints{$c2} = $c2; + } +CONSTRAINT: + for my $c1 ( $t1->get_constraints ) { + for my $c2 ( keys %table2_constraints ) { + $c2 = $table2_constraints{$c2}; + if ( $c1->equals($c2, $case_insensitive) ) { + delete $table2_constraints{$c2}; + next CONSTRAINT; + } + } + push @diffs_constraint_adds, "ALTER TABLE $t1_name ADD ". + constraint_to_string($c1, $source_schema).";"; + } + for my $c2 ( keys %table2_constraints ) { + $c2 = $table2_constraints{$c2}; + if ( $c2->type eq UNIQUE ) { + push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP INDEX ". + $c2->name.";"; + } elsif ( $target_db =~ /SQLServer/ ) { + push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->name.";"; + } else { + push @diffs_constraint_drops, "ALTER TABLE $t1_name DROP ".$c2->type. + ($c2->type eq FOREIGN_KEY ? " ".$c2->name : '').";"; + } + } + + push @diffs, @diffs_index_drops, @diffs_constraint_drops, + @diffs_table_options, @diffs_table_adds, @diffs_table_changes, + @diffs_constraint_adds, @diffs_index_creates; } for my $t2 ( $target_schema->get_tables ) { my $t2_name = $t2->name; - my $t1 = $source_schema->get_table( $t2_name ); + my $t1 = $source_schema->get_table( $t2_name, $target_db =~ /SQLServer/ ); unless ( $t1 ) { - push @diffs, "DROP TABLE $t2_name;"; + if ( $target_db =~ /SQLServer/ ) { + for my $constraint ( $t2->get_constraints ) { + next if $constraint->type eq PRIMARY_KEY; + push @diffs, "ALTER TABLE $t2_name DROP ".$constraint->name.";"; + } + } + push @diffs_at_end, "DROP TABLE $t2_name;"; next; } @@ -289,7 +417,8 @@ for my $t2 ( $target_schema->get_tables ) { my $f2_name = $t2_field->name; my $t1_field = $t1->get_field( $f2_name ); unless ( $t1_field ) { - push @diffs, "ALTER TABLE $t2_name DROP $f2_name;"; + my $modifier = $target_db =~ /SQLServer/ ? "COLUMN " : ''; + push @diffs, "ALTER TABLE $t2_name DROP $modifier$f2_name;"; } } } @@ -300,6 +429,7 @@ if ( @new_tables ) { my $producer = $dummy_tr->producer( $target_db ); unshift @diffs, $producer->( $dummy_tr ); } +push(@diffs, @diffs_at_end); if ( @diffs ) { print join( "\n", @@ -310,6 +440,65 @@ else { print "There were no differences.\n"; } +sub constraint_to_string { + my $c = shift; + my $schema = shift or die "No schema given"; + my @fields = $c->fields or return ''; + + if ( $c->type eq PRIMARY_KEY ) { + return 'PRIMARY KEY (' . join(', ', @fields). ')'; + } + elsif ( $c->type eq UNIQUE ) { + return 'UNIQUE '. + (defined $c->name ? $c->name.' ' : ''). + '(' . join(', ', @fields). ')'; + } + elsif ( $c->type eq FOREIGN_KEY ) { + my $def = join(' ', + map { $_ || () } 'CONSTRAINT', $c->name, 'FOREIGN KEY' + ); + + $def .= ' (' . join( ', ', @fields ) . ')'; + + $def .= ' REFERENCES ' . $c->reference_table; + + my @rfields = map { $_ || () } $c->reference_fields; + unless ( @rfields ) { + my $rtable_name = $c->reference_table; + if ( my $ref_table = $schema->get_table( $rtable_name ) ) { + push @rfields, $ref_table->primary_key; + } + else { + warn "Can't find reference table '$rtable_name' " . + "in schema\n"; + } + } + + if ( @rfields ) { + $def .= ' (' . join( ', ', @rfields ) . ')'; + } + else { + warn "FK constraint on " . 'some table' . '.' . + join('', @fields) . " has no reference fields\n"; + } + + if ( $c->match_type ) { + $def .= ' MATCH ' . + ( $c->match_type =~ /full/i ) ? 'FULL' : 'PARTIAL'; + } + + if ( $c->on_delete ) { + $def .= ' ON DELETE '.join( ' ', $c->on_delete ); + } + + if ( $c->on_update ) { + $def .= ' ON UPDATE '.join( ' ', $c->on_update ); + } + + return $def; + } +} + # ------------------------------------------------------------------- # Bring out number weight & measure in a year of dearth. # William Blake