view Lib/IMPL/SQL/Schema/Traits.pm @ 59:0f3e369553bd

Rewritten property implementation (probably become slower but more flexible) Configuration infrastructure in progress (in the aspect of the lazy activation) Initial concept for the code generator
author wizard
date Tue, 09 Mar 2010 02:50:45 +0300
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;