A whole lot of changes, but major additions include adding diffs for table options...
Chris Hilton [Mon, 27 Jun 2005 22:09:42 +0000 (22:09 +0000)]
Many smaller changes for various quirks of SQL Server and MySQL

bin/sqlt-diff

index d354801..0ab4cba 100755 (executable)
@@ -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