#!/usr/bin/perl -w # vim: set ft=perl: # ------------------------------------------------------------------- # $Id: sqlt-diff,v 1.3 2004-02-06 17:38:56 kycl4rk Exp $ # ------------------------------------------------------------------- # Copyright (C) 2002 The 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 sqlt-diff - find the differences b/w two schemas =head1 SYNOPSIS For help: sqlt-diff -h|--help For a list of all valid parsers: sqlt -l|--list To diff two schemas: sqlt-diff [options] file_name1=parser file_name2=parser Options: -d|--debug Show debugging info =head1 DESCRIPTION sqlt-diff is a utility for creating a file of SQL commands necessary to transform the first schema provided to the second. While not yet exhaustive in its ability to mutate the entire schema, it will report the following =over =item * New tables Using the Producer class of the target (second) schema, any tables missing in the first schema will be generated in their entirety (fields, constraints, indices). =item * Missing/altered fields Any fields missing or altered between the two schemas will be reported as: ALTER TABLE [DROP ] [CHANGE ()] ; =item * Missing/altered indices Any indices missing or of a different type or on different fields will be indicated. Indices that should be dropped will be reported as such: DROP INDEX ON ; An index of a different type or on different fields will be reported as a new index as such: CREATE [] INDEX [] ON ( [,] ) ; =back "ALTER/DROP TABLE" and "CREATE INDEX" statements B generated by the Producer, unfortunately, and may require massaging before being passed to your target database. =cut # ------------------------------------------------------------------- use strict; use Pod::Usage; use Data::Dumper; use SQL::Translator; use SQL::Translator::Schema::Constants; use vars qw( $VERSION ); $VERSION = sprintf "%d.%02d", q$Revision: 1.3 $ =~ /(\d+)\.(\d+)/; my ( @input, $list, $help, $debug ); for my $arg ( @ARGV ) { if ( $arg =~ m/^-?-l(ist)?$/ ) { $list = 1; } elsif ( $arg =~ m/^-?-h(elp)?$/ ) { $help = 1; } elsif ( $arg =~ m/^-?-d(ebug)?$/ ) { $debug = 1; } elsif ( $arg =~ m/^([^=]+)=(.+)$/ ) { push @input, { file => $1, parser => $2 }; } else { pod2usage( msg => "Unknown argument '$arg'" ); } } pod2usage(1) if $help; pod2usage('Please specify only two schemas to diff') if scalar @input > 2; my $tr = SQL::Translator->new; my @parsers = $tr->list_parsers; my %valid_parsers = map { $_, 1 } @parsers; if ( $list ) { print "\nParsers:\n", map { "\t$_\n" } sort @parsers; print "\n"; exit(0); } pod2usage( msg => 'Too many file args' ) if @input > 2; my ( $source_schema, $source_db, $target_schema, $target_db ); my $i = 2; for my $in ( @input ) { my $file = $in->{'file'}; my $parser = $in->{'parser'}; die "Unable to read file '$file'\n" unless -r $file; die "'$parser' is an invalid parser\n" unless $valid_parsers{ $parser }; my $t = SQL::Translator->new; $t->debug( $debug ); $t->parser( $parser ) or die $tr->error; $t->producer( 'YAML' ) or die $tr->error; my $out = $t->translate( $file ) or die $tr->error; my $schema = $t->schema; unless ( $schema->name ) { $schema->name( $file ); } if ( $i == 1 ) { $source_schema = $schema; $source_db = $parser; } else { $target_schema = $schema; $target_db = $parser; } $i--; } my $s1_name = $source_schema->name; my $s2_name = $target_schema->name; my ( @new_tables, @diffs ); for my $t1 ( $source_schema->get_tables ) { my $t1_name = $t1->name; my $t2 = $target_schema->get_table( $t1_name ); warn "Checking '$s1_name' table '$t1_name'\n" if $debug; unless ( $t2 ) { push @new_tables, $t1; next; } my $t2_name = $t2->name; for my $t1_field ( $t1->get_fields ) { my $t1_type = $t1_field->data_type; my $t1_size = $t1_field->size; my $t1_name = $t1_field->name; my $t2_field = $t2->get_field( $t1_name ); my $f1_full_name = "$s1_name.$t1_name.$t1_name"; warn "Checking '$f1_full_name'\n" if $debug; my $f2_full_name = "$s2_name.$t2_name.$t1_name"; unless ( $t2_field ) { push @diffs, "ALTER TABLE $t1_name ADD $t1_name $t1_type($t1_size);"; next; } my $t2_type = $t2_field->data_type; my $t2_size = $t2_field->size; if ( $t1_type ne $t2_type || ( defined $t1_size && ( $t1_size ne $t2_size ) ) ) { push @diffs, "ALTER TABLE $t1_name CHANGE $t1_name $t1_type($t1_size);"; } } 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; } } } 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( "CREATE %sINDEX%s ON %s (%s);", $type eq NORMAL ? '' : "$type ", $iname ? " $iname" : '', $t1_name, $fields, ); } } } for my $t2 ( $target_schema->get_tables ) { my $t2_name = $t2->name; my $t1 = $source_schema->get_table( $t2_name ); unless ( $t1 ) { push @diffs, "DROP TABLE $t2_name;"; next; } for my $t2_field ( $t2->get_fields ) { 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;"; } } } if ( @new_tables ) { my $dummy_tr = SQL::Translator->new; $dummy_tr->schema->add_table( $_ ) for @new_tables; my $producer = $dummy_tr->producer( $target_db ); unshift @diffs, $producer->( $dummy_tr ); } if ( @diffs ) { print join( "\n", "-- Convert schema '$s2_name' to '$s1_name':", @diffs, '' ); } else { print "There were no differences.\n"; } # ------------------------------------------------------------------- # Bring out number weight & measure in a year of dearth. # William Blake # ------------------------------------------------------------------- =pod =head1 AUTHOR Ken Y. Clark Ekclark@cpan.orgE. =head1 SEE ALSO SQL::Translator, L. =cut