Mercurial > pub > Impl
comparison Lib/IMPL/SQL/Schema/Traits.pm @ 32:56cef8e3cda6
+1
| author | Sergey |
|---|---|
| date | Mon, 09 Nov 2009 01:39:31 +0300 |
| parents | |
| children | 32d2350fccf9 |
comparison
equal
deleted
inserted
replaced
| 31:d59526f6310e | 32:56cef8e3cda6 |
|---|---|
| 1 package IMPL::SQL::Schema::Traits; | |
| 2 use strict; | |
| 3 use base qw(IMPL::Object IMPL::Object::Autofill); | |
| 4 use IMPL::Class::Property; | |
| 5 use IMPL::Class::Property::Direct; | |
| 6 | |
| 7 use constant { | |
| 8 STATE_NORMAL => 0, | |
| 9 STATE_UPDATED => 1, | |
| 10 STATE_CREATED => 2, | |
| 11 STATE_REMOVED => 3, | |
| 12 STATE_PENDING => 4 | |
| 13 } ; | |
| 14 | |
| 15 BEGIN { | |
| 16 public _direct property SrcSchema => prop_none; | |
| 17 public _direct property DstSchema => prop_none; | |
| 18 public _direct property PendingActions => prop_get; | |
| 19 public _direct property TableInfo => prop_get; | |
| 20 public _direct property Handler => prop_get; | |
| 21 public _direct property TableMap => prop_none; | |
| 22 public _direct property KeepTables => prop_all; | |
| 23 } | |
| 24 | |
| 25 __PACKAGE__->PassThroughArgs; | |
| 26 | |
| 27 sub CTOR { | |
| 28 my $this = shift; | |
| 29 | |
| 30 $this->{$SrcSchema} or die new IMPL::InvalidArgumentException('A source schema is required'); | |
| 31 $this->{$DstSchema} or die new IMPL::InvalidArgumentException('A destination schema is required'); | |
| 32 $this->{$Handler} or die new IMPL::InvalidArgumentException('A handler is required to produce the update batch'); | |
| 33 | |
| 34 $this->{$TableInfo} = {}; | |
| 35 $this->{$PendingActions} = []; | |
| 36 | |
| 37 } | |
| 38 | |
| 39 sub UpdateTable { | |
| 40 my ($this,$srcTable) = @_; | |
| 41 | |
| 42 return 1 if $this->{$TableInfo}->{$srcTable->Name}->{'processed'}; | |
| 43 | |
| 44 my $dstTableName = $this->{$TableMap}->{$srcTable->Name} ? $this->{$TableMap}->{$srcTable->Name} : $srcTable->Name; | |
| 45 my $dstTable = $this->{$DstSchema}->Tables->{$dstTableName}; | |
| 46 | |
| 47 $this->{$TableInfo}->{$srcTable->Name}->{'processed'} = 1; | |
| 48 | |
| 49 if (not $dstTable) { | |
| 50 $this->DropTable($srcTable) if not $this->{$KeepTables}; | |
| 51 return 1; | |
| 52 } | |
| 53 | |
| 54 if ( not grep {$srcTable->Column($_->Name)} $dstTable->Columns ) { | |
| 55 | |
| 56 $this->{$TableInfo}->{$srcTable->Name}->{'NewName'} = $dstTable->Name if $srcTable->Name ne $dstTable->Name; | |
| 57 | |
| 58 $this->DropTable($srcTable); | |
| 59 $this->CreateTable($dstTable); | |
| 60 | |
| 61 return 1; | |
| 62 } | |
| 63 | |
| 64 if ($srcTable->Name ne $dstTableName) { | |
| 65 $this->RenameTable($srcTable,$dstTableName); | |
| 66 } | |
| 67 | |
| 68 my %dstConstraints = %{$dstTable->Constraints}; | |
| 69 | |
| 70 foreach my $srcConstraint (values %{$srcTable->Constraints}) { | |
| 71 if (my $dstConstraint = delete $dstConstraints{$srcConstraint->Name}) { | |
| 72 $this->UpdateConstraint($srcConstraint,$dstConstraint); | |
| 73 } else { | |
| 74 $this->DropConstraint($srcConstraint); | |
| 75 } | |
| 76 } | |
| 77 | |
| 78 my $i = 0; | |
| 79 my %dstColumns = map { $_->Name, $i++} $dstTable->Columns ; | |
| 80 | |
| 81 # сначала удаляем столбцы | |
| 82 # потом добавляем недостающие и изменяем столбцы в нужном порядке | |
| 83 | |
| 84 my @columnsToUpdate; | |
| 85 | |
| 86 foreach my $srcColumn ($srcTable->Columns) { | |
| 87 if (defined (my $dstColumnIndex = delete $dstColumns{$srcColumn->Name})) { | |
| 88 push @columnsToUpdate, { Action => 'update', ColumnSrc => $srcColumn, ColumnDst => $dstTable->ColumnAt($dstColumnIndex), NewPosition => $dstColumnIndex}; | |
| 89 } else { | |
| 90 $this->DropColumn($srcTable,$srcColumn); | |
| 91 } | |
| 92 } | |
| 93 push @columnsToUpdate, map { {Action => 'add', ColumnDst => $dstTable->ColumnAt($_), NewPosition => $_} } values %dstColumns; | |
| 94 | |
| 95 foreach my $action (sort {$a->{'NewPosition'} <=> $b->{'NewPosition'}} @columnsToUpdate ) { | |
| 96 if ($action->{'Action'} eq 'update') { | |
| 97 $this->UpdateColumn($srcTable,@$action{'ColumnSrc','ColumnDst'},$dstTable,$action->{'NewPosition'}); # change type and position | |
| 98 }elsif ($action->{'Action'} eq 'add') { | |
| 99 $this->AddColumn($srcTable,$action->{'ColumnDst'},$dstTable,$action->{'NewPosition'}); # add at specified position | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 foreach my $dstConstraint (values %dstConstraints) { | |
| 104 $this->AddConstraint($dstConstraint); | |
| 105 } | |
| 106 | |
| 107 $this->{$TableInfo}{$srcTable->Name}{'State'} = STATE_UPDATED; | |
| 108 } | |
| 109 | |
| 110 sub UpdateConstraint { | |
| 111 my ($this,$src,$dst) = @_; | |
| 112 | |
| 113 if (not ConstraintEquals($src,$dst)) { | |
| 114 if (UNIVERSAL::isa($src,'IMPL::SQL::Schema::Constraint::PrimaryKey')) { | |
| 115 $this->UpdateTable($_->Table) foreach values %{$src->ConnectedFK}; | |
| 116 } | |
| 117 $this->DropConstraint($src); | |
| 118 $this->AddConstraint($dst); | |
| 119 } else { | |
| 120 $this->{$TableInfo}->{$this->MapTableName($src->Table->Name)}->{'Constraints'}->{$src->Name} = STATE_UPDATED; | |
| 121 } | |
| 122 } | |
| 123 | |
| 124 sub ConstraintEquals { | |
| 125 my ($src,$dst) = @_; | |
| 126 | |
| 127 ref $src eq ref $dst or return 0; | |
| 128 | |
| 129 my @dstColumns = $dst->Columns; | |
| 130 scalar(@{$src->Columns}) == scalar(@{$dst->Columns}) and not grep { my $column = shift @dstColumns; not $column->isSame($_) } $src->Columns or return 0; | |
| 131 | |
| 132 not UNIVERSAL::isa($src,'IMPL::SQL::Schema::Constraint::ForeignKey') or ConstraintEquals($src->ReferencedPrimaryKey,$dst->ReferencedPrimaryKey) or return 0; | |
| 133 | |
| 134 1; | |
| 135 } | |
| 136 | |
| 137 sub UpdateSchema { | |
| 138 my ($this) = @_; | |
| 139 | |
| 140 my %Updated = map { $this->UpdateTable($_); $this->MapTableName($_->Name) , 1; } values %{$this->{$SrcSchema}->Tables ? $this->{$SrcSchema}->Tables : {} }; | |
| 141 | |
| 142 $this->CreateTable($_) foreach grep {not $Updated{$_->Name}} values %{$this->{$DstSchema}->Tables}; | |
| 143 | |
| 144 $this->ProcessPendingActions(); | |
| 145 } | |
| 146 | |
| 147 sub RenameTable { | |
| 148 my ($this,$tblSrc,$tblDstName) = @_; | |
| 149 | |
| 150 $this->{$Handler}->AlterTableRename($tblSrc->Name,$tblDstName); | |
| 151 $this->{$TableInfo}->{$tblSrc->Name}->{'NewName'} = $tblDstName; | |
| 152 } | |
| 153 | |
| 154 sub MapTableName { | |
| 155 my ($this,$srcName) = @_; | |
| 156 | |
| 157 $this->{$TableInfo}->{$srcName}->{'NewName'} ? $this->{$TableInfo}->{$srcName}->{'NewName'} : $srcName; | |
| 158 } | |
| 159 | |
| 160 sub DropTable { | |
| 161 my ($this,$tbl) = @_; | |
| 162 | |
| 163 if ($tbl->PrimaryKey) { | |
| 164 $this->UpdateTable($_->Table) foreach values %{$tbl->PrimaryKey->ConnectedFK}; | |
| 165 } | |
| 166 | |
| 167 $this->{$Handler}->DropTable($this->MapTableName($tbl->Name)); | |
| 168 $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'State'} = STATE_REMOVED; | |
| 169 $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'Constraints'} = {map {$_,STATE_REMOVED} keys %{$tbl->Constraints}}; | |
| 170 $this->{$TableInfo}{$this->MapTableName($tbl->Name)}{'Columns'} = {map { $_->Name, STATE_REMOVED} $tbl->Columns}; | |
| 171 | |
| 172 return 1; | |
| 173 } | |
| 174 | |
| 175 sub CreateTable { | |
| 176 my ($this,$tbl) = @_; | |
| 177 | |
| 178 # создаем таблицу, кроме внешних ключей | |
| 179 $this->{$Handler}->CreateTable($tbl,skip_foreign_keys => 1); | |
| 180 | |
| 181 $this->{$TableInfo}->{$tbl->Name}->{'State'} = STATE_CREATED; | |
| 182 | |
| 183 $this->{$TableInfo}->{$tbl->Name}->{'Columns'} = {map { $_->Name, STATE_CREATED } $tbl->Columns}; | |
| 184 $this->{$TableInfo}->{$tbl->Name}->{'Constraints'} = {map {$_->Name, STATE_CREATED} grep { not UNIVERSAL::isa($_,'IMPL::SQL::Schema::Constraint::ForeignKey') } values %{$tbl->Constraints}}; | |
| 185 | |
| 186 $this->AddConstraint($_) foreach grep { UNIVERSAL::isa($_,'IMPL::SQL::Schema::Constraint::ForeignKey') } values %{$tbl->Constraints}; | |
| 187 | |
| 188 return 1; | |
| 189 } | |
| 190 | |
| 191 sub AddColumn { | |
| 192 my ($this,$tblSrc,$column,$tblDst,$pos) = @_; | |
| 193 | |
| 194 $this->{$Handler}->AlterTableAddColumn($this->MapTableName($tblSrc->Name),$column,$tblDst,$pos); | |
| 195 $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$column->Name} = STATE_CREATED; | |
| 196 | |
| 197 return 1; | |
| 198 } | |
| 199 | |
| 200 sub DropColumn { | |
| 201 my ($this,$tblSrc,$column) = @_; | |
| 202 $this->{$Handler}->AlterTableDropColumn($this->MapTableName($tblSrc->Name),$column->Name); | |
| 203 $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$column->Name} = STATE_REMOVED; | |
| 204 | |
| 205 return 1; | |
| 206 } | |
| 207 | |
| 208 sub UpdateColumn { | |
| 209 my ($this,$tblSrc,$srcColumn,$dstColumn,$tblDst,$pos) = @_; | |
| 210 | |
| 211 if ($srcColumn->isSame($dstColumn) and $pos < @{$tblSrc->Columns} and $tblSrc->ColumnAt($pos) == $srcColumn) { | |
| 212 $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$dstColumn->Name} = STATE_UPDATED; | |
| 213 return 1; | |
| 214 } | |
| 215 | |
| 216 $this->{$Handler}->AlterTableChangeColumn($this->MapTableName($tblSrc->Name),$dstColumn,$tblDst,$pos); | |
| 217 $this->{$TableInfo}->{$this->MapTableName($tblSrc->Name)}->{'Columns'}->{$dstColumn->Name} = STATE_UPDATED; | |
| 218 | |
| 219 return 1; | |
| 220 } | |
| 221 | |
| 222 sub DropConstraint { | |
| 223 my ($this,$constraint) = @_; | |
| 224 | |
| 225 $this->{$Handler}->AlterTableDropConstraint($this->MapTableName($constraint->Table->Name),$constraint); | |
| 226 $this->{$TableInfo}->{$constraint->Table->Name}->{'Constraints'}->{$constraint->Name} = STATE_REMOVED; | |
| 227 | |
| 228 return 1; | |
| 229 } | |
| 230 | |
| 231 sub IfUndef { | |
| 232 my ($value,$default) = @_; | |
| 233 | |
| 234 return defined $value ? $value : $default; | |
| 235 } | |
| 236 | |
| 237 sub AddConstraint { | |
| 238 my ($this,$constraint) = @_; | |
| 239 | |
| 240 # перед добавлением ограничения нужно убедиться в том, что созданы все необходимые столбцы и сопутствующие | |
| 241 # ограничения (например первичные ключи) | |
| 242 | |
| 243 my $pending; | |
| 244 | |
| 245 $pending = grep { my $column = $_; not grep { IfUndef($this->{$TableInfo}{$constraint->Table->Name}{'Columns'}{$column->Name}, STATE_NORMAL) == $_ } (STATE_UPDATED, STATE_CREATED) } $constraint->Columns; | |
| 246 | |
| 247 if ($pending) { | |
| 248 push @{$this->{$PendingActions}},{Action => \&AddConstraint, Args => [$constraint]}; | |
| 249 return 2; | |
| 250 } else { | |
| 251 if (UNIVERSAL::isa($constraint,'IMPL::SQL::Schema::Constraint::ForeignKey')) { | |
| 252 if (not grep { IfUndef($this->{$TableInfo}{$constraint->ReferencedPrimaryKey->Table->Name}{'Constraints'}{$constraint->ReferencedPrimaryKey->Name},STATE_NORMAL) == $_} (STATE_UPDATED, STATE_CREATED)) { | |
| 253 push @{$this->{$PendingActions}},{Action => \&AddConstraint, Args => [$constraint]}; | |
| 254 return 2; | |
| 255 } | |
| 256 } | |
| 257 $this->{$Handler}->AlterTableAddConstraint($constraint->Table->Name,$constraint); | |
| 258 $this->{$TableInfo}->{$constraint->Table->Name}->{'Constraints'}->{$constraint->Name} = STATE_CREATED; | |
| 259 } | |
| 260 } | |
| 261 | |
| 262 sub ProcessPendingActions { | |
| 263 my ($this) = @_; | |
| 264 | |
| 265 while (my $action = shift @{$this->{$PendingActions}}) { | |
| 266 $action->{'Action'}->($this,@{$action->{'Args'}}); | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 1; |
