Mercurial > pub > Impl
view Lib/Schema/DB/Traits.pm @ 94:79bf75223afe
Fixed security related bugs
author | wizard |
---|---|
date | Thu, 29 Apr 2010 01:31:27 +0400 |
parents | 16ada169ca75 |
children |
line wrap: on
line source
package Schema::DB::Traits; use strict; use Common; our @ISA = qw (Object); use constant { STATE_NORMAL => 0, STATE_UPDATED => 1, STATE_CREATED => 2, STATE_REMOVED => 3, STATE_PENDING => 4 } ; BEGIN { DeclareProperty SrcSchema => ACCESS_NONE; DeclareProperty DstSchema => ACCESS_NONE; DeclareProperty PendingActions => ACCESS_READ; DeclareProperty TableInfo => ACCESS_READ; DeclareProperty Handler => ACCESS_READ; DeclareProperty TableMap => ACCESS_NONE; DeclareProperty KeepTables => ACCESS_ALL; } sub CTOR { my $this = shift; $this->SUPER::CTOR(@_); $this->{$SrcSchema} or die new Exception('A source schema is required'); $this->{$DstSchema} or die new Exception('A destination schema is required'); $this->{$Handler} or die new Exception('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,'Schema::DB::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,'Schema::DB::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($_,'Schema::DB::Constraint::ForeignKey') } values %{$tbl->Constraints}}; $this->AddConstraint($_) foreach grep { UNIVERSAL::isa($_,'Schema::DB::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,'Schema::DB::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;