view Lib/IMPL/SQL/Schema/Traits.pm @ 134:44977efed303

Significant performance optimizations Fixed recursion problems due converting objects to JSON Added cache support for the templates Added discovery feature for the web methods
author wizard
date Mon, 21 Jun 2010 02:39:53 +0400
parents 16ada169ca75
children 6ce1f052b90a
line wrap: on
line source

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_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;