Mercurial > pub > Impl
diff Lib/IMPL/SQL/Schema/Traits.pm @ 32:56cef8e3cda6
+1
author | Sergey |
---|---|
date | Mon, 09 Nov 2009 01:39:31 +0300 |
parents | |
children | 32d2350fccf9 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/IMPL/SQL/Schema/Traits.pm Mon Nov 09 01:39:31 2009 +0300 @@ -0,0 +1,270 @@ +package IMPL::SQL::Schema::Traits; +use strict; +use base qw(IMPL::Object IMPL::Object::Autofill); +use IMPL::Class::Property; +use IMPL::Class::Property::Direct; + +use constant { + STATE_NORMAL => 0, + STATE_UPDATED => 1, + STATE_CREATED => 2, + STATE_REMOVED => 3, + STATE_PENDING => 4 +} ; + +BEGIN { + public _direct property SrcSchema => prop_none; + public _direct property DstSchema => prop_none; + public _direct property PendingActions => prop_get; + public _direct property TableInfo => prop_get; + public _direct property Handler => prop_get; + public _direct property TableMap => prop_none; + public _direct property KeepTables => prop_all; +} + +__PACKAGE__->PassThroughArgs; + +sub CTOR { + my $this = shift; + + $this->{$SrcSchema} or die new IMPL::InvalidArgumentException('A source schema is required'); + $this->{$DstSchema} or die new IMPL::InvalidArgumentException('A destination schema is required'); + $this->{$Handler} or die new IMPL::InvalidArgumentException('A handler is required to produce the update batch'); + + $this->{$TableInfo} = {}; + $this->{$PendingActions} = []; + +} + +sub UpdateTable { + my ($this,$srcTable) = @_; + + return 1 if $this->{$TableInfo}->{$srcTable->Name}->{'processed'}; + + my $dstTableName = $this->{$TableMap}->{$srcTable->Name} ? $this->{$TableMap}->{$srcTable->Name} : $srcTable->Name; + my $dstTable = $this->{$DstSchema}->Tables->{$dstTableName}; + + $this->{$TableInfo}->{$srcTable->Name}->{'processed'} = 1; + + if (not $dstTable) { + $this->DropTable($srcTable) if not $this->{$KeepTables}; + return 1; + } + + if ( not grep {$srcTable->Column($_->Name)} $dstTable->Columns ) { + + $this->{$TableInfo}->{$srcTable->Name}->{'NewName'} = $dstTable->Name if $srcTable->Name ne $dstTable->Name; + + $this->DropTable($srcTable); + $this->CreateTable($dstTable); + + return 1; + } + + if ($srcTable->Name ne $dstTableName) { + $this->RenameTable($srcTable,$dstTableName); + } + + my %dstConstraints = %{$dstTable->Constraints}; + + foreach my $srcConstraint (values %{$srcTable->Constraints}) { + if (my $dstConstraint = delete $dstConstraints{$srcConstraint->Name}) { + $this->UpdateConstraint($srcConstraint,$dstConstraint); + } else { + $this->DropConstraint($srcConstraint); + } + } + + my $i = 0; + my %dstColumns = map { $_->Name, $i++} $dstTable->Columns ; + + # сначала удаляем столбцы + # потом добавляем недостающие и изменяем столбцы в нужном порядке + + my @columnsToUpdate; + + foreach my $srcColumn ($srcTable->Columns) { + if (defined (my $dstColumnIndex = delete $dstColumns{$srcColumn->Name})) { + push @columnsToUpdate, { Action => 'update', ColumnSrc => $srcColumn, ColumnDst => $dstTable->ColumnAt($dstColumnIndex), NewPosition => $dstColumnIndex}; + } else { + $this->DropColumn($srcTable,$srcColumn); + } + } + push @columnsToUpdate, map { {Action => 'add', ColumnDst => $dstTable->ColumnAt($_), NewPosition => $_} } values %dstColumns; + + foreach my $action (sort {$a->{'NewPosition'} <=> $b->{'NewPosition'}} @columnsToUpdate ) { + if ($action->{'Action'} eq 'update') { + $this->UpdateColumn($srcTable,@$action{'ColumnSrc','ColumnDst'},$dstTable,$action->{'NewPosition'}); # change type and position + }elsif ($action->{'Action'} eq 'add') { + $this->AddColumn($srcTable,$action->{'ColumnDst'},$dstTable,$action->{'NewPosition'}); # add at specified position + } + } + + foreach my $dstConstraint (values %dstConstraints) { + $this->AddConstraint($dstConstraint); + } + + $this->{$TableInfo}{$srcTable->Name}{'State'} = STATE_UPDATED; +} + +sub UpdateConstraint { + my ($this,$src,$dst) = @_; + + if (not ConstraintEquals($src,$dst)) { + if (UNIVERSAL::isa($src,'IMPL::SQL::Schema::Constraint::PrimaryKey')) { + $this->UpdateTable($_->Table) foreach values %{$src->ConnectedFK}; + } + $this->DropConstraint($src); + $this->AddConstraint($dst); + } else { + $this->{$TableInfo}->{$this->MapTableName($src->Table->Name)}->{'Constraints'}->{$src->Name} = STATE_UPDATED; + } +} + +sub ConstraintEquals { + my ($src,$dst) = @_; + + ref $src eq ref $dst or return 0; + + my @dstColumns = $dst->Columns; + scalar(@{$src->Columns}) == scalar(@{$dst->Columns}) and not grep { my $column = shift @dstColumns; not $column->isSame($_) } $src->Columns or return 0; + + not UNIVERSAL::isa($src,'IMPL::SQL::Schema::Constraint::ForeignKey') or ConstraintEquals($src->ReferencedPrimaryKey,$dst->ReferencedPrimaryKey) or return 0; + + 1; +} + +sub UpdateSchema { + my ($this) = @_; + + my %Updated = map { $this->UpdateTable($_); $this->MapTableName($_->Name) , 1; } values %{$this->{$SrcSchema}->Tables ? $this->{$SrcSchema}->Tables : {} }; + + $this->CreateTable($_) foreach grep {not $Updated{$_->Name}} values %{$this->{$DstSchema}->Tables}; + + $this->ProcessPendingActions(); +} + +sub RenameTable { + my ($this,$tblSrc,$tblDstName) = @_; + + $this->{$Handler}->AlterTableRename($tblSrc->Name,$tblDstName); + $this->{$TableInfo}->{$tblSrc->Name}->{'NewName'} = $tblDstName; +} + +sub MapTableName { + my ($this,$srcName) = @_; + + $this->{$TableInfo}->{$srcName}->{'NewName'} ? $this->{$TableInfo}->{$srcName}->{'NewName'} : $srcName; +} + +sub DropTable { + my ($this,$tbl) = @_; + + if ($tbl->PrimaryKey) { + $this->UpdateTable($_->Table) foreach values %{$tbl->PrimaryKey->ConnectedFK}; + } + + $this->{$Handler}->DropTable($this->MapTableName($tbl->Name)); + $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'State'} = STATE_REMOVED; + $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'Constraints'} = {map {$_,STATE_REMOVED} keys %{$tbl->Constraints}}; + $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'Columns'} = {map { $_->Name, STATE_REMOVED} $tbl->Columns}; + + return 1; +} + +sub CreateTable { + my ($this,$tbl) = @_; + + # создаем таблицу, кроме внешних ключей + $this->{$Handler}->CreateTable($tbl,skip_foreign_keys => 1); + + $this->{$TableInfo}->{$tbl->Name}->{'State'} = STATE_CREATED; + + $this->{$TableInfo}->{$tbl->Name}->{'Columns'} = {map { $_->Name, STATE_CREATED } $tbl->Columns}; + $this->{$TableInfo}->{$tbl->Name}->{'Constraints'} = {map {$_->Name, STATE_CREATED} grep { not UNIVERSAL::isa($_,'IMPL::SQL::Schema::Constraint::ForeignKey') } values %{$tbl->Constraints}}; + + $this->AddConstraint($_) foreach grep { UNIVERSAL::isa($_,'IMPL::SQL::Schema::Constraint::ForeignKey') } values %{$tbl->Constraints}; + + return 1; +} + +sub AddColumn { + my ($this,$tblSrc,$column,$tblDst,$pos) = @_; + + $this->{$Handler}->AlterTableAddColumn($this->MapTableName($tblSrc->Name),$column,$tblDst,$pos); + $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$column->Name} = STATE_CREATED; + + return 1; +} + +sub DropColumn { + my ($this,$tblSrc,$column) = @_; + $this->{$Handler}->AlterTableDropColumn($this->MapTableName($tblSrc->Name),$column->Name); + $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$column->Name} = STATE_REMOVED; + + return 1; +} + +sub UpdateColumn { + my ($this,$tblSrc,$srcColumn,$dstColumn,$tblDst,$pos) = @_; + + if ($srcColumn->isSame($dstColumn) and $pos < @{$tblSrc->Columns} and $tblSrc->ColumnAt($pos) == $srcColumn) { + $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$dstColumn->Name} = STATE_UPDATED; + return 1; + } + + $this->{$Handler}->AlterTableChangeColumn($this->MapTableName($tblSrc->Name),$dstColumn,$tblDst,$pos); + $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$dstColumn->Name} = STATE_UPDATED; + + return 1; +} + +sub DropConstraint { + my ($this,$constraint) = @_; + + $this->{$Handler}->AlterTableDropConstraint($this->MapTableName($constraint->Table->Name),$constraint); + $this->{$TableInfo}->{$constraint->Table->Name}->{'Constraints'}->{$constraint->Name} = STATE_REMOVED; + + return 1; +} + +sub IfUndef { + my ($value,$default) = @_; + + return defined $value ? $value : $default; +} + +sub AddConstraint { + my ($this,$constraint) = @_; + + # перед добавлением ограничения нужно убедиться в том, что созданы все необходимые столбцы и сопутствующие + # ограничения (например первичные ключи) + + my $pending; + + $pending = grep { my $column = $_; not grep { IfUndef($this->{$TableInfo}{$constraint->Table->Name}{'Columns'}{$column->Name}, STATE_NORMAL) == $_ } (STATE_UPDATED, STATE_CREATED) } $constraint->Columns; + + if ($pending) { + push @{$this->{$PendingActions}},{Action => \&AddConstraint, Args => [$constraint]}; + return 2; + } else { + if (UNIVERSAL::isa($constraint,'IMPL::SQL::Schema::Constraint::ForeignKey')) { + if (not grep { IfUndef($this->{$TableInfo}{$constraint->ReferencedPrimaryKey->Table->Name}{'Constraints'}{$constraint->ReferencedPrimaryKey->Name},STATE_NORMAL) == $_} (STATE_UPDATED, STATE_CREATED)) { + push @{$this->{$PendingActions}},{Action => \&AddConstraint, Args => [$constraint]}; + return 2; + } + } + $this->{$Handler}->AlterTableAddConstraint($constraint->Table->Name,$constraint); + $this->{$TableInfo}->{$constraint->Table->Name}->{'Constraints'}->{$constraint->Name} = STATE_CREATED; + } +} + +sub ProcessPendingActions { + my ($this) = @_; + + while (my $action = shift @{$this->{$PendingActions}}) { + $action->{'Action'}->($this,@{$action->{'Args'}}); + } +} + +1;