view Lib/Schema/DB/Traits.pm @ 31:d59526f6310e

Small fixes to Test framework (correct handlinf of the compilation errors in the test units) Imported and refactored SQL DB schema from the old project
author Sergey
date Mon, 09 Nov 2009 01:39:16 +0300
parents 03e58a454b20
children 16ada169ca75
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;