view Lib/IMPL/SQL/Schema/TraitsOld.pm @ 250:129e48bb5afb

DOM refactoring ObjectToDOM methods are virtual QueryToDOM uses inflators Fixed transform for the complex values in the ObjectToDOM QueryToDOM doesn't allow to use complex values (HASHes) as values for nodes (overpost problem)
author sergey
date Wed, 07 Nov 2012 04:17:53 +0400
parents d1676be8afcc
children
line wrap: on
line source

package IMPL::SQL::Schema::Traits;
use strict;
use parent 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_all;
    public _direct property DstSchema => prop_all;
    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;