|
1 <?php |
|
2 /* |
|
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
14 * |
|
15 * This software consists of voluntary contributions made by many individuals |
|
16 * and is licensed under the LGPL. For more information, see |
|
17 * <http://www.doctrine-project.org>. |
|
18 */ |
|
19 |
|
20 namespace Doctrine\DBAL\Schema; |
|
21 |
|
22 /** |
|
23 * Compare to Schemas and return an instance of SchemaDiff |
|
24 * |
|
25 * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. |
|
26 * @license http://ez.no/licenses/new_bsd New BSD License |
|
27 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
|
28 * @link www.doctrine-project.org |
|
29 * @since 2.0 |
|
30 * @version $Revision$ |
|
31 * @author Benjamin Eberlei <kontakt@beberlei.de> |
|
32 */ |
|
33 class Comparator |
|
34 { |
|
35 /** |
|
36 * @param Schema $fromSchema |
|
37 * @param Schema $toSchema |
|
38 * @return SchemaDiff |
|
39 */ |
|
40 static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) |
|
41 { |
|
42 $c = new self(); |
|
43 return $c->compare($fromSchema, $toSchema); |
|
44 } |
|
45 |
|
46 /** |
|
47 * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. |
|
48 * |
|
49 * The returned diferences are returned in such a way that they contain the |
|
50 * operations to change the schema stored in $fromSchema to the schema that is |
|
51 * stored in $toSchema. |
|
52 * |
|
53 * @param Schema $fromSchema |
|
54 * @param Schema $toSchema |
|
55 * |
|
56 * @return SchemaDiff |
|
57 */ |
|
58 public function compare(Schema $fromSchema, Schema $toSchema) |
|
59 { |
|
60 $diff = new SchemaDiff(); |
|
61 |
|
62 $foreignKeysToTable = array(); |
|
63 |
|
64 foreach ( $toSchema->getTables() AS $tableName => $table ) { |
|
65 if ( !$fromSchema->hasTable($tableName) ) { |
|
66 $diff->newTables[$tableName] = $table; |
|
67 } else { |
|
68 $tableDifferences = $this->diffTable( $fromSchema->getTable($tableName), $table ); |
|
69 if ( $tableDifferences !== false ) { |
|
70 $diff->changedTables[$tableName] = $tableDifferences; |
|
71 } |
|
72 } |
|
73 } |
|
74 |
|
75 /* Check if there are tables removed */ |
|
76 foreach ( $fromSchema->getTables() AS $tableName => $table ) { |
|
77 if ( !$toSchema->hasTable($tableName) ) { |
|
78 $diff->removedTables[$tableName] = $table; |
|
79 } |
|
80 |
|
81 // also remember all foreign keys that point to a specific table |
|
82 foreach ($table->getForeignKeys() AS $foreignKey) { |
|
83 $foreignTable = strtolower($foreignKey->getForeignTableName()); |
|
84 if (!isset($foreignKeysToTable[$foreignTable])) { |
|
85 $foreignKeysToTable[$foreignTable] = array(); |
|
86 } |
|
87 $foreignKeysToTable[$foreignTable][] = $foreignKey; |
|
88 } |
|
89 } |
|
90 |
|
91 foreach ($diff->removedTables AS $tableName => $table) { |
|
92 if (isset($foreignKeysToTable[$tableName])) { |
|
93 $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]); |
|
94 } |
|
95 } |
|
96 |
|
97 foreach ( $toSchema->getSequences() AS $sequenceName => $sequence) { |
|
98 if (!$fromSchema->hasSequence($sequenceName)) { |
|
99 $diff->newSequences[] = $sequence; |
|
100 } else { |
|
101 if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { |
|
102 $diff->changedSequences[] = $toSchema->getSequence($sequenceName); |
|
103 } |
|
104 } |
|
105 } |
|
106 |
|
107 foreach ($fromSchema->getSequences() AS $sequenceName => $sequence) { |
|
108 if (!$toSchema->hasSequence($sequenceName)) { |
|
109 $diff->removedSequences[] = $sequence; |
|
110 } |
|
111 } |
|
112 |
|
113 return $diff; |
|
114 } |
|
115 |
|
116 /** |
|
117 * |
|
118 * @param Sequence $sequence1 |
|
119 * @param Sequence $sequence2 |
|
120 */ |
|
121 public function diffSequence(Sequence $sequence1, Sequence $sequence2) |
|
122 { |
|
123 if($sequence1->getAllocationSize() != $sequence2->getAllocationSize()) { |
|
124 return true; |
|
125 } |
|
126 |
|
127 if($sequence1->getInitialValue() != $sequence2->getInitialValue()) { |
|
128 return true; |
|
129 } |
|
130 |
|
131 return false; |
|
132 } |
|
133 |
|
134 /** |
|
135 * Returns the difference between the tables $table1 and $table2. |
|
136 * |
|
137 * If there are no differences this method returns the boolean false. |
|
138 * |
|
139 * @param Table $table1 |
|
140 * @param Table $table2 |
|
141 * |
|
142 * @return bool|TableDiff |
|
143 */ |
|
144 public function diffTable(Table $table1, Table $table2) |
|
145 { |
|
146 $changes = 0; |
|
147 $tableDifferences = new TableDiff($table1->getName()); |
|
148 |
|
149 $table1Columns = $table1->getColumns(); |
|
150 $table2Columns = $table2->getColumns(); |
|
151 |
|
152 /* See if all the fields in table 1 exist in table 2 */ |
|
153 foreach ( $table2Columns as $columnName => $column ) { |
|
154 if ( !$table1->hasColumn($columnName) ) { |
|
155 $tableDifferences->addedColumns[$columnName] = $column; |
|
156 $changes++; |
|
157 } |
|
158 } |
|
159 /* See if there are any removed fields in table 2 */ |
|
160 foreach ( $table1Columns as $columnName => $column ) { |
|
161 if ( !$table2->hasColumn($columnName) ) { |
|
162 $tableDifferences->removedColumns[$columnName] = $column; |
|
163 $changes++; |
|
164 } |
|
165 } |
|
166 |
|
167 foreach ( $table1Columns as $columnName => $column ) { |
|
168 if ( $table2->hasColumn($columnName) ) { |
|
169 $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); |
|
170 if (count($changedProperties) ) { |
|
171 $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); |
|
172 $tableDifferences->changedColumns[$column->getName()] = $columnDiff; |
|
173 $changes++; |
|
174 } |
|
175 } |
|
176 } |
|
177 |
|
178 $this->detectColumnRenamings($tableDifferences); |
|
179 |
|
180 $table1Indexes = $table1->getIndexes(); |
|
181 $table2Indexes = $table2->getIndexes(); |
|
182 |
|
183 foreach ($table2Indexes AS $index2Name => $index2Definition) { |
|
184 foreach ($table1Indexes AS $index1Name => $index1Definition) { |
|
185 if ($this->diffIndex($index1Definition, $index2Definition) === false) { |
|
186 unset($table1Indexes[$index1Name]); |
|
187 unset($table2Indexes[$index2Name]); |
|
188 } else { |
|
189 if ($index1Name == $index2Name) { |
|
190 $tableDifferences->changedIndexes[$index2Name] = $table2Indexes[$index2Name]; |
|
191 unset($table1Indexes[$index1Name]); |
|
192 unset($table2Indexes[$index2Name]); |
|
193 $changes++; |
|
194 } |
|
195 } |
|
196 } |
|
197 } |
|
198 |
|
199 foreach ($table1Indexes AS $index1Name => $index1Definition) { |
|
200 $tableDifferences->removedIndexes[$index1Name] = $index1Definition; |
|
201 $changes++; |
|
202 } |
|
203 |
|
204 foreach ($table2Indexes AS $index2Name => $index2Definition) { |
|
205 $tableDifferences->addedIndexes[$index2Name] = $index2Definition; |
|
206 $changes++; |
|
207 } |
|
208 |
|
209 $fromFkeys = $table1->getForeignKeys(); |
|
210 $toFkeys = $table2->getForeignKeys(); |
|
211 |
|
212 foreach ($fromFkeys AS $key1 => $constraint1) { |
|
213 foreach ($toFkeys AS $key2 => $constraint2) { |
|
214 if($this->diffForeignKey($constraint1, $constraint2) === false) { |
|
215 unset($fromFkeys[$key1]); |
|
216 unset($toFkeys[$key2]); |
|
217 } else { |
|
218 if (strtolower($constraint1->getName()) == strtolower($constraint2->getName())) { |
|
219 $tableDifferences->changedForeignKeys[] = $constraint2; |
|
220 $changes++; |
|
221 unset($fromFkeys[$key1]); |
|
222 unset($toFkeys[$key2]); |
|
223 } |
|
224 } |
|
225 } |
|
226 } |
|
227 |
|
228 foreach ($fromFkeys AS $key1 => $constraint1) { |
|
229 $tableDifferences->removedForeignKeys[] = $constraint1; |
|
230 $changes++; |
|
231 } |
|
232 |
|
233 foreach ($toFkeys AS $key2 => $constraint2) { |
|
234 $tableDifferences->addedForeignKeys[] = $constraint2; |
|
235 $changes++; |
|
236 } |
|
237 |
|
238 return $changes ? $tableDifferences : false; |
|
239 } |
|
240 |
|
241 /** |
|
242 * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop |
|
243 * however ambiguouties between different possibilites should not lead to renaming at all. |
|
244 * |
|
245 * @param TableDiff $tableDifferences |
|
246 */ |
|
247 private function detectColumnRenamings(TableDiff $tableDifferences) |
|
248 { |
|
249 $renameCandidates = array(); |
|
250 foreach ($tableDifferences->addedColumns AS $addedColumnName => $addedColumn) { |
|
251 foreach ($tableDifferences->removedColumns AS $removedColumnName => $removedColumn) { |
|
252 if (count($this->diffColumn($addedColumn, $removedColumn)) == 0) { |
|
253 $renameCandidates[$addedColumn->getName()][] = array($removedColumn, $addedColumn, $addedColumnName); |
|
254 } |
|
255 } |
|
256 } |
|
257 |
|
258 foreach ($renameCandidates AS $candidate => $candidateColumns) { |
|
259 if (count($candidateColumns) == 1) { |
|
260 list($removedColumn, $addedColumn) = $candidateColumns[0]; |
|
261 $removedColumnName = strtolower($removedColumn->getName()); |
|
262 $addedColumnName = strtolower($addedColumn->getName()); |
|
263 |
|
264 $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; |
|
265 unset($tableDifferences->addedColumns[$addedColumnName]); |
|
266 unset($tableDifferences->removedColumns[$removedColumnName]); |
|
267 } |
|
268 } |
|
269 } |
|
270 |
|
271 /** |
|
272 * @param ForeignKeyConstraint $key1 |
|
273 * @param ForeignKeyConstraint $key2 |
|
274 * @return bool |
|
275 */ |
|
276 public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) |
|
277 { |
|
278 if (array_map('strtolower', $key1->getLocalColumns()) != array_map('strtolower', $key2->getLocalColumns())) { |
|
279 return true; |
|
280 } |
|
281 |
|
282 if (array_map('strtolower', $key1->getForeignColumns()) != array_map('strtolower', $key2->getForeignColumns())) { |
|
283 return true; |
|
284 } |
|
285 |
|
286 if ($key1->onUpdate() != $key2->onUpdate()) { |
|
287 return true; |
|
288 } |
|
289 |
|
290 if ($key1->onDelete() != $key2->onDelete()) { |
|
291 return true; |
|
292 } |
|
293 |
|
294 return false; |
|
295 } |
|
296 |
|
297 /** |
|
298 * Returns the difference between the fields $field1 and $field2. |
|
299 * |
|
300 * If there are differences this method returns $field2, otherwise the |
|
301 * boolean false. |
|
302 * |
|
303 * @param Column $column1 |
|
304 * @param Column $column2 |
|
305 * |
|
306 * @return array |
|
307 */ |
|
308 public function diffColumn(Column $column1, Column $column2) |
|
309 { |
|
310 $changedProperties = array(); |
|
311 if ( $column1->getType() != $column2->getType() ) { |
|
312 $changedProperties[] = 'type'; |
|
313 } |
|
314 |
|
315 if ($column1->getNotnull() != $column2->getNotnull()) { |
|
316 $changedProperties[] = 'notnull'; |
|
317 } |
|
318 |
|
319 if ($column1->getDefault() != $column2->getDefault()) { |
|
320 $changedProperties[] = 'default'; |
|
321 } |
|
322 |
|
323 if ($column1->getUnsigned() != $column2->getUnsigned()) { |
|
324 $changedProperties[] = 'unsigned'; |
|
325 } |
|
326 |
|
327 if ($column1->getType() instanceof \Doctrine\DBAL\Types\StringType) { |
|
328 // check if value of length is set at all, default value assumed otherwise. |
|
329 $length1 = $column1->getLength() ?: 255; |
|
330 $length2 = $column2->getLength() ?: 255; |
|
331 if ($length1 != $length2) { |
|
332 $changedProperties[] = 'length'; |
|
333 } |
|
334 |
|
335 if ($column1->getFixed() != $column2->getFixed()) { |
|
336 $changedProperties[] = 'fixed'; |
|
337 } |
|
338 } |
|
339 |
|
340 if ($column1->getType() instanceof \Doctrine\DBAL\Types\DecimalType) { |
|
341 if (($column1->getPrecision()?:10) != ($column2->getPrecision()?:10)) { |
|
342 $changedProperties[] = 'precision'; |
|
343 } |
|
344 if ($column1->getScale() != $column2->getScale()) { |
|
345 $changedProperties[] = 'scale'; |
|
346 } |
|
347 } |
|
348 |
|
349 if ($column1->getAutoincrement() != $column2->getAutoincrement()) { |
|
350 $changedProperties[] = 'autoincrement'; |
|
351 } |
|
352 |
|
353 // only allow to delete comment if its set to '' not to null. |
|
354 if ($column1->getComment() !== null && $column1->getComment() != $column2->getComment()) { |
|
355 $changedProperties[] = 'comment'; |
|
356 } |
|
357 |
|
358 return $changedProperties; |
|
359 } |
|
360 |
|
361 /** |
|
362 * Finds the difference between the indexes $index1 and $index2. |
|
363 * |
|
364 * Compares $index1 with $index2 and returns $index2 if there are any |
|
365 * differences or false in case there are no differences. |
|
366 * |
|
367 * @param Index $index1 |
|
368 * @param Index $index2 |
|
369 * @return bool |
|
370 */ |
|
371 public function diffIndex(Index $index1, Index $index2) |
|
372 { |
|
373 if ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1)) { |
|
374 return false; |
|
375 } |
|
376 return true; |
|
377 } |
|
378 } |