# 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
#
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 ) {
}
$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;
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;
}
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;";
}
}
}
my $producer = $dummy_tr->producer( $target_db );
unshift @diffs, $producer->( $dummy_tr );
}
+push(@diffs, @diffs_at_end);
if ( @diffs ) {
print join( "\n",
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