view Lib/IMPL/SQL/Schema/Traits/Diff.pm @ 192:a9faf883cdce

IMPL::Web::View refactoring
author cin
date Fri, 06 Apr 2012 16:22:38 +0400
parents d1676be8afcc
children 4d0e1962161c
line wrap: on
line source

package IMPL::SQL::Schema::Traits::Diff;
use strict;
use warnings;
use IMPL::lang qw(:compare :hash is);

use IMPL::SQL::Schema();
use IMPL::SQL::Schema::Traits();

use constant {
	schema_t => typeof IMPL::SQL::Schema # defining a constant is a good style to enable compile checks
};

sub Diff {
	my ($self,$src,$dst) = @_;
	
	die new IMPL::InvalidArgumentException( src => "A valid source schema is required") unless is($src,schema_t);
	die new IMPL::InvalidArgumentException( dst => "A valid desctination schema is requried" ) unless is($src,schema_t);
	
	my %dstTables = map { $_->name, $_ } $dst->GetTables;
	
	my @operations;
	
	foreach my $srcTable ( $src->GetTables) {
		my $dstTable = delete $dstTables{$srcTable->name};
		
		if (not $dstTable) {
			# if a source table doesn't have a corresponding destination table, it should be deleted
			push @operations, new IMPL::SQL::Schema::Traits::DropTable($srcTable->name);
		} else {
			# a source table needs to be updated
			push @operations, $self->_DiffTables($srcTable,$dstTable);
		}
		
	}
	
	foreach my $tbl ( values %dstTables ) {
		push @operations, new IMPL::SQL::Schema::Traits::CreateTable(
			new IMPL::SQL::Schema::Traits::Table(
				$tbl->name,
				[ map _Column2Traits($_), @{$tbl->columns} ],
				[ map _Constraint2Traits($_), $tbl->GetConstraints()],
				$tbl->{tag}
			)
		)
	}
	
	return \@operations;
}

sub _DiffTables {
	my ($self,$src,$dst) = @_;
	
	my @dropConstraints;
	my @createConstraints;
	
	my %srcConstraints = map { $_->name, $_ } $src->GetConstraints();
	my %dstConstraints = map { $_->name, $_ } $dst->GetConstraints();
	
	foreach my $cnSrcName (keys %srcConstraints) {
		if ( my $cnDst = delete $dstConstraints{$cnSrcName} ) {
			unless ( $srcConstraints{$cnSrcName}->SameValue($cnDst) ) {
				push @dropConstraints,
					new IMPL::SQL::Schema::Traits::AlterTableDropConstraint( $src->name, $cnSrcName );
				push @createConstraints,
					new IMPL::SQL::Schema::Traits::AlterTableAddConstraint( $dst->name, _Constraint2Traits($cnDst) );
			}
		} else {
			push @dropConstraints,new IMPL::SQL::Schema::Traits::AlterTableDropConstraint( $src->name, $cnSrcName );
		}
	}
	
	foreach my $cnDst (values %dstConstraints) {
		push @createConstraints,
		IMPL::SQL::Schema::Traits::AlterTableAddConstraint->new( $dst->name, _Constraint2Traits($cnDst) );
	}
	
	my @deleteColumns;
	my @addColumns;
	my @updateColumns;
	
	my %dstColumnIndexes = map {
		my $col = $dst->GetColumnAt($_);
		($col->name, { column => $col, index => $_ })
	} 0 .. $dst->ColumnsCount-1;
	
	my @columns;
	
	# remove old columns, mark for update changed columns
	for( my $i=0; $i < $src->ColumnsCount; $i++) {
		my $colSrc = $src->GetColumnAt($i);
		
		if ( my $infoDst = delete $dstColumnIndexes{$colSrc->name} ) {
			$infoDst->{prevColumn} = $colSrc;
			push @columns,$infoDst;
		} else {
			push @deleteColumns,new IMPL::SQL::Schema::Traits::AlterTableDropColumn($src->name,$colSrc->name);
		}
	}
	
	#insert new columns at specified positions
	foreach ( sort { $a->{index} <=> $b->{index} } values %dstColumnIndexes ) {
		splice(@columns,$_->{index},0,$_);
		push @addColumns, new IMPL::SQL::Schema::Traits::AlterTableAddColumn($src->name, _Column2Traits( $_->{column}, position => $_->{index} ));
	}
	
	# remember old indexes
	for(my $i =0; $i< @columns; $i ++) {
		$columns[$i]->{prevIndex} = $i;
	}
	
	# reorder columns
	@columns = sort { $a->{index} <=> $b->{index} } @columns;
	
	foreach my $info (@columns) {
		if ($info->{prevColumn} && ( !$info->{column}->SameValue($info->{prevColumn}) or $info->{index}!= $info->{prevIndex} ) ) {
			my $op = new IMPL::SQL::Schema::Traits::AlterTableChangeColumn($src->name,$info->{column}->name);

			$op->position( $info->{index} ) unless $info->{prevIndex} == $info->{index};
			$op->isNullable( $info->{column}->isNullable ) unless equals($info->{column}->isNullable,$info->{prevColumn}->isNullable);
			$op->defaultValue( $info->{column}->defaultValue ) unless equals($info->{column}->defaultValue, $info->{prevColumn}->defaultValue);
			
			my $diff = hashDiff($info->{prevColumn}->tag,$info->{column}->tag);
			$op->options($diff) if %$diff;
			
			push @updateColumns, $op;
		}
	}
	
	my @result = (@dropConstraints, @deleteColumns, @addColumns, @updateColumns, @createConstraints); 
	
	return @result;
}

sub _Column2Traits {
	my ($column,%options) = @_;
	
	return new IMPL::SQL::Schema::Traits::Column(
		$column->name,
		$column->type,
		isNullable => $column->isNullable,
		defaultValue => $column->defaultValue,
		tag => $column->tag,
		%options
	);
}

sub _Constraint2Traits {
	my ($constraint) = @_;
	
	my $map = {
		typeof IMPL::SQL::Schema::Constraint::ForeignKey , typeof IMPL::SQL::Schema::Traits::ForeignKey,
		typeof IMPL::SQL::Schema::Constraint::PrimaryKey , typeof IMPL::SQL::Schema::Traits::PrimaryKey,
		typeof IMPL::SQL::Schema::Constraint::Unique , typeof IMPL::SQL::Schema::Traits::Unique,
		typeof IMPL::SQL::Schema::Constraint::Index , typeof IMPL::SQL::Schema::Traits::Index
	};
	
	my $class = $map->{$constraint->typeof} or die new IMPL::Exception("Can't map the constraint",$constraint->typeof);
	
	return $class->new(
		$constraint->name,
		[ map $_->name, $constraint->columns ]
	)
}

1;