PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Doctrine/DBAL/Schema/Comparator.php

http://github.com/doctrine/dbal
PHP | 538 lines | 354 code | 93 blank | 91 comment | 65 complexity | 967b35ff7f3b46cf6688ef0d4f613140 MD5 | raw file
Possible License(s): Unlicense
  1. <?php
  2. namespace Doctrine\DBAL\Schema;
  3. use Doctrine\DBAL\Types;
  4. use function array_intersect_key;
  5. use function array_key_exists;
  6. use function array_keys;
  7. use function array_map;
  8. use function array_merge;
  9. use function array_shift;
  10. use function array_unique;
  11. use function assert;
  12. use function count;
  13. use function get_class;
  14. use function strtolower;
  15. /**
  16. * Compares two Schemas and return an instance of SchemaDiff.
  17. */
  18. class Comparator
  19. {
  20. /**
  21. * @return SchemaDiff
  22. */
  23. public static function compareSchemas(Schema $fromSchema, Schema $toSchema)
  24. {
  25. $c = new self();
  26. return $c->compare($fromSchema, $toSchema);
  27. }
  28. /**
  29. * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema.
  30. *
  31. * The returned differences are returned in such a way that they contain the
  32. * operations to change the schema stored in $fromSchema to the schema that is
  33. * stored in $toSchema.
  34. *
  35. * @return SchemaDiff
  36. */
  37. public function compare(Schema $fromSchema, Schema $toSchema)
  38. {
  39. $diff = new SchemaDiff();
  40. $diff->fromSchema = $fromSchema;
  41. $foreignKeysToTable = [];
  42. foreach ($toSchema->getNamespaces() as $namespace) {
  43. if ($fromSchema->hasNamespace($namespace)) {
  44. continue;
  45. }
  46. $diff->newNamespaces[$namespace] = $namespace;
  47. }
  48. foreach ($fromSchema->getNamespaces() as $namespace) {
  49. if ($toSchema->hasNamespace($namespace)) {
  50. continue;
  51. }
  52. $diff->removedNamespaces[$namespace] = $namespace;
  53. }
  54. foreach ($toSchema->getTables() as $table) {
  55. $tableName = $table->getShortestName($toSchema->getName());
  56. if (! $fromSchema->hasTable($tableName)) {
  57. $diff->newTables[$tableName] = $toSchema->getTable($tableName);
  58. } else {
  59. $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName));
  60. if ($tableDifferences !== false) {
  61. $diff->changedTables[$tableName] = $tableDifferences;
  62. }
  63. }
  64. }
  65. /* Check if there are tables removed */
  66. foreach ($fromSchema->getTables() as $table) {
  67. $tableName = $table->getShortestName($fromSchema->getName());
  68. $table = $fromSchema->getTable($tableName);
  69. if (! $toSchema->hasTable($tableName)) {
  70. $diff->removedTables[$tableName] = $table;
  71. }
  72. // also remember all foreign keys that point to a specific table
  73. foreach ($table->getForeignKeys() as $foreignKey) {
  74. $foreignTable = strtolower($foreignKey->getForeignTableName());
  75. if (! isset($foreignKeysToTable[$foreignTable])) {
  76. $foreignKeysToTable[$foreignTable] = [];
  77. }
  78. $foreignKeysToTable[$foreignTable][] = $foreignKey;
  79. }
  80. }
  81. foreach ($diff->removedTables as $tableName => $table) {
  82. if (! isset($foreignKeysToTable[$tableName])) {
  83. continue;
  84. }
  85. $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]);
  86. // deleting duplicated foreign keys present on both on the orphanedForeignKey
  87. // and the removedForeignKeys from changedTables
  88. foreach ($foreignKeysToTable[$tableName] as $foreignKey) {
  89. // strtolower the table name to make if compatible with getShortestName
  90. $localTableName = strtolower($foreignKey->getLocalTableName());
  91. if (! isset($diff->changedTables[$localTableName])) {
  92. continue;
  93. }
  94. foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) {
  95. assert($removedForeignKey instanceof ForeignKeyConstraint);
  96. // We check if the key is from the removed table if not we skip.
  97. if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) {
  98. continue;
  99. }
  100. unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]);
  101. }
  102. }
  103. }
  104. foreach ($toSchema->getSequences() as $sequence) {
  105. $sequenceName = $sequence->getShortestName($toSchema->getName());
  106. if (! $fromSchema->hasSequence($sequenceName)) {
  107. if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {
  108. $diff->newSequences[] = $sequence;
  109. }
  110. } else {
  111. if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {
  112. $diff->changedSequences[] = $toSchema->getSequence($sequenceName);
  113. }
  114. }
  115. }
  116. foreach ($fromSchema->getSequences() as $sequence) {
  117. if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {
  118. continue;
  119. }
  120. $sequenceName = $sequence->getShortestName($fromSchema->getName());
  121. if ($toSchema->hasSequence($sequenceName)) {
  122. continue;
  123. }
  124. $diff->removedSequences[] = $sequence;
  125. }
  126. return $diff;
  127. }
  128. /**
  129. * @param Schema $schema
  130. * @param Sequence $sequence
  131. *
  132. * @return bool
  133. */
  134. private function isAutoIncrementSequenceInSchema($schema, $sequence)
  135. {
  136. foreach ($schema->getTables() as $table) {
  137. if ($sequence->isAutoIncrementsFor($table)) {
  138. return true;
  139. }
  140. }
  141. return false;
  142. }
  143. /**
  144. * @return bool
  145. */
  146. public function diffSequence(Sequence $sequence1, Sequence $sequence2)
  147. {
  148. if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
  149. return true;
  150. }
  151. return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
  152. }
  153. /**
  154. * Returns the difference between the tables $table1 and $table2.
  155. *
  156. * If there are no differences this method returns the boolean false.
  157. *
  158. * @return TableDiff|false
  159. */
  160. public function diffTable(Table $table1, Table $table2)
  161. {
  162. $changes = 0;
  163. $tableDifferences = new TableDiff($table1->getName());
  164. $tableDifferences->fromTable = $table1;
  165. $table1Columns = $table1->getColumns();
  166. $table2Columns = $table2->getColumns();
  167. /* See if all the fields in table 1 exist in table 2 */
  168. foreach ($table2Columns as $columnName => $column) {
  169. if ($table1->hasColumn($columnName)) {
  170. continue;
  171. }
  172. $tableDifferences->addedColumns[$columnName] = $column;
  173. $changes++;
  174. }
  175. /* See if there are any removed fields in table 2 */
  176. foreach ($table1Columns as $columnName => $column) {
  177. // See if column is removed in table 2.
  178. if (! $table2->hasColumn($columnName)) {
  179. $tableDifferences->removedColumns[$columnName] = $column;
  180. $changes++;
  181. continue;
  182. }
  183. // See if column has changed properties in table 2.
  184. $changedProperties = $this->diffColumn($column, $table2->getColumn($columnName));
  185. if (empty($changedProperties)) {
  186. continue;
  187. }
  188. $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties);
  189. $columnDiff->fromColumn = $column;
  190. $tableDifferences->changedColumns[$column->getName()] = $columnDiff;
  191. $changes++;
  192. }
  193. $this->detectColumnRenamings($tableDifferences);
  194. $table1Indexes = $table1->getIndexes();
  195. $table2Indexes = $table2->getIndexes();
  196. /* See if all the indexes in table 1 exist in table 2 */
  197. foreach ($table2Indexes as $indexName => $index) {
  198. if (($index->isPrimary() && $table1->hasPrimaryKey()) || $table1->hasIndex($indexName)) {
  199. continue;
  200. }
  201. $tableDifferences->addedIndexes[$indexName] = $index;
  202. $changes++;
  203. }
  204. /* See if there are any removed indexes in table 2 */
  205. foreach ($table1Indexes as $indexName => $index) {
  206. // See if index is removed in table 2.
  207. if (($index->isPrimary() && ! $table2->hasPrimaryKey()) ||
  208. ! $index->isPrimary() && ! $table2->hasIndex($indexName)
  209. ) {
  210. $tableDifferences->removedIndexes[$indexName] = $index;
  211. $changes++;
  212. continue;
  213. }
  214. // See if index has changed in table 2.
  215. $table2Index = $index->isPrimary() ? $table2->getPrimaryKey() : $table2->getIndex($indexName);
  216. assert($table2Index instanceof Index);
  217. if (! $this->diffIndex($index, $table2Index)) {
  218. continue;
  219. }
  220. $tableDifferences->changedIndexes[$indexName] = $table2Index;
  221. $changes++;
  222. }
  223. $this->detectIndexRenamings($tableDifferences);
  224. $fromFkeys = $table1->getForeignKeys();
  225. $toFkeys = $table2->getForeignKeys();
  226. foreach ($fromFkeys as $key1 => $constraint1) {
  227. foreach ($toFkeys as $key2 => $constraint2) {
  228. if ($this->diffForeignKey($constraint1, $constraint2) === false) {
  229. unset($fromFkeys[$key1], $toFkeys[$key2]);
  230. } else {
  231. if (strtolower($constraint1->getName()) === strtolower($constraint2->getName())) {
  232. $tableDifferences->changedForeignKeys[] = $constraint2;
  233. $changes++;
  234. unset($fromFkeys[$key1], $toFkeys[$key2]);
  235. }
  236. }
  237. }
  238. }
  239. foreach ($fromFkeys as $constraint1) {
  240. $tableDifferences->removedForeignKeys[] = $constraint1;
  241. $changes++;
  242. }
  243. foreach ($toFkeys as $constraint2) {
  244. $tableDifferences->addedForeignKeys[] = $constraint2;
  245. $changes++;
  246. }
  247. return $changes ? $tableDifferences : false;
  248. }
  249. /**
  250. * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
  251. * however ambiguities between different possibilities should not lead to renaming at all.
  252. *
  253. * @return void
  254. */
  255. private function detectColumnRenamings(TableDiff $tableDifferences)
  256. {
  257. $renameCandidates = [];
  258. foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) {
  259. foreach ($tableDifferences->removedColumns as $removedColumn) {
  260. if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) {
  261. continue;
  262. }
  263. $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
  264. }
  265. }
  266. foreach ($renameCandidates as $candidateColumns) {
  267. if (count($candidateColumns) !== 1) {
  268. continue;
  269. }
  270. [$removedColumn, $addedColumn] = $candidateColumns[0];
  271. $removedColumnName = strtolower($removedColumn->getName());
  272. $addedColumnName = strtolower($addedColumn->getName());
  273. if (isset($tableDifferences->renamedColumns[$removedColumnName])) {
  274. continue;
  275. }
  276. $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn;
  277. unset(
  278. $tableDifferences->addedColumns[$addedColumnName],
  279. $tableDifferences->removedColumns[$removedColumnName]
  280. );
  281. }
  282. }
  283. /**
  284. * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
  285. * however ambiguities between different possibilities should not lead to renaming at all.
  286. *
  287. * @return void
  288. */
  289. private function detectIndexRenamings(TableDiff $tableDifferences)
  290. {
  291. $renameCandidates = [];
  292. // Gather possible rename candidates by comparing each added and removed index based on semantics.
  293. foreach ($tableDifferences->addedIndexes as $addedIndexName => $addedIndex) {
  294. foreach ($tableDifferences->removedIndexes as $removedIndex) {
  295. if ($this->diffIndex($addedIndex, $removedIndex)) {
  296. continue;
  297. }
  298. $renameCandidates[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
  299. }
  300. }
  301. foreach ($renameCandidates as $candidateIndexes) {
  302. // If the current rename candidate contains exactly one semantically equal index,
  303. // we can safely rename it.
  304. // Otherwise it is unclear if a rename action is really intended,
  305. // therefore we let those ambiguous indexes be added/dropped.
  306. if (count($candidateIndexes) !== 1) {
  307. continue;
  308. }
  309. [$removedIndex, $addedIndex] = $candidateIndexes[0];
  310. $removedIndexName = strtolower($removedIndex->getName());
  311. $addedIndexName = strtolower($addedIndex->getName());
  312. if (isset($tableDifferences->renamedIndexes[$removedIndexName])) {
  313. continue;
  314. }
  315. $tableDifferences->renamedIndexes[$removedIndexName] = $addedIndex;
  316. unset(
  317. $tableDifferences->addedIndexes[$addedIndexName],
  318. $tableDifferences->removedIndexes[$removedIndexName]
  319. );
  320. }
  321. }
  322. /**
  323. * @return bool
  324. */
  325. public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2)
  326. {
  327. if (array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns())) {
  328. return true;
  329. }
  330. if (array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns())) {
  331. return true;
  332. }
  333. if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
  334. return true;
  335. }
  336. if ($key1->onUpdate() !== $key2->onUpdate()) {
  337. return true;
  338. }
  339. return $key1->onDelete() !== $key2->onDelete();
  340. }
  341. /**
  342. * Returns the difference between the fields $field1 and $field2.
  343. *
  344. * If there are differences this method returns $field2, otherwise the
  345. * boolean false.
  346. *
  347. * @return string[]
  348. */
  349. public function diffColumn(Column $column1, Column $column2)
  350. {
  351. $properties1 = $column1->toArray();
  352. $properties2 = $column2->toArray();
  353. $changedProperties = [];
  354. if (get_class($properties1['type']) !== get_class($properties2['type'])) {
  355. $changedProperties[] = 'type';
  356. }
  357. foreach (['notnull', 'unsigned', 'autoincrement'] as $property) {
  358. if ($properties1[$property] === $properties2[$property]) {
  359. continue;
  360. }
  361. $changedProperties[] = $property;
  362. }
  363. // This is a very nasty hack to make comparator work with the legacy json_array type, which should be killed in v3
  364. if ($this->isALegacyJsonComparison($properties1['type'], $properties2['type'])) {
  365. array_shift($changedProperties);
  366. $changedProperties[] = 'comment';
  367. }
  368. // Null values need to be checked additionally as they tell whether to create or drop a default value.
  369. // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
  370. if (($properties1['default'] === null) !== ($properties2['default'] === null)
  371. || $properties1['default'] != $properties2['default']) {
  372. $changedProperties[] = 'default';
  373. }
  374. if (($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) ||
  375. $properties1['type'] instanceof Types\BinaryType
  376. ) {
  377. // check if value of length is set at all, default value assumed otherwise.
  378. $length1 = $properties1['length'] ?: 255;
  379. $length2 = $properties2['length'] ?: 255;
  380. if ($length1 !== $length2) {
  381. $changedProperties[] = 'length';
  382. }
  383. if ($properties1['fixed'] !== $properties2['fixed']) {
  384. $changedProperties[] = 'fixed';
  385. }
  386. } elseif ($properties1['type'] instanceof Types\DecimalType) {
  387. if (($properties1['precision'] ?: 10) !== ($properties2['precision'] ?: 10)) {
  388. $changedProperties[] = 'precision';
  389. }
  390. if ($properties1['scale'] !== $properties2['scale']) {
  391. $changedProperties[] = 'scale';
  392. }
  393. }
  394. // A null value and an empty string are actually equal for a comment so they should not trigger a change.
  395. if ($properties1['comment'] !== $properties2['comment'] &&
  396. ! ($properties1['comment'] === null && $properties2['comment'] === '') &&
  397. ! ($properties2['comment'] === null && $properties1['comment'] === '')
  398. ) {
  399. $changedProperties[] = 'comment';
  400. }
  401. $customOptions1 = $column1->getCustomSchemaOptions();
  402. $customOptions2 = $column2->getCustomSchemaOptions();
  403. foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) {
  404. if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) {
  405. $changedProperties[] = $key;
  406. } elseif ($properties1[$key] !== $properties2[$key]) {
  407. $changedProperties[] = $key;
  408. }
  409. }
  410. $platformOptions1 = $column1->getPlatformOptions();
  411. $platformOptions2 = $column2->getPlatformOptions();
  412. foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) {
  413. if ($properties1[$key] === $properties2[$key]) {
  414. continue;
  415. }
  416. $changedProperties[] = $key;
  417. }
  418. return array_unique($changedProperties);
  419. }
  420. /**
  421. * TODO: kill with fire on v3.0
  422. *
  423. * @deprecated
  424. */
  425. private function isALegacyJsonComparison(Types\Type $one, Types\Type $other) : bool
  426. {
  427. if (! $one instanceof Types\JsonType || ! $other instanceof Types\JsonType) {
  428. return false;
  429. }
  430. return ( ! $one instanceof Types\JsonArrayType && $other instanceof Types\JsonArrayType)
  431. || ( ! $other instanceof Types\JsonArrayType && $one instanceof Types\JsonArrayType);
  432. }
  433. /**
  434. * Finds the difference between the indexes $index1 and $index2.
  435. *
  436. * Compares $index1 with $index2 and returns $index2 if there are any
  437. * differences or false in case there are no differences.
  438. *
  439. * @return bool
  440. */
  441. public function diffIndex(Index $index1, Index $index2)
  442. {
  443. return ! ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1));
  444. }
  445. }