[FEATURE] Doctrine: Implement SchemaMigrationService
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Functional / Database / Schema / SchemaMigrationServiceTest.php
1 <?php
2 declare(strict_types=1);
3
4 namespace TYPO3\CMS\Core\Tests\Functional\Category\Collection;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use Doctrine\DBAL\Schema\AbstractSchemaManager;
20 use Doctrine\DBAL\Schema\Table;
21 use Doctrine\DBAL\Types\BigIntType;
22 use Doctrine\DBAL\Types\IntegerType;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
25 use TYPO3\CMS\Core\Database\Schema\SqlReader;
26 use TYPO3\CMS\Core\Tests\FunctionalTestCase;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29 /**
30 * Test case for \TYPO3\CMS\Core\Database\Schema\SchemaMigrationServiceTest
31 */
32 class SchemaMigrationServiceTest extends FunctionalTestCase
33 {
34 /**
35 * @var SqlReader
36 */
37 protected $sqlReader;
38
39 /**
40 * @var ConnectionPool
41 */
42 protected $connectionPool;
43
44 /**
45 * @var AbstractSchemaManager
46 */
47 protected $schemaManager;
48
49 /**
50 * @var \TYPO3\CMS\Core\Database\Schema\SchemaMigrator
51 */
52 protected $subject;
53
54 /**
55 * @var string
56 */
57 protected $tableName = 'aTestTable';
58
59 /**
60 * Sets up this test suite.
61 *
62 * @return void
63 */
64 protected function setUp()
65 {
66 parent::setUp();
67 $this->subject = GeneralUtility::makeInstance(SchemaMigrator::class);
68 $this->sqlReader = GeneralUtility::makeInstance(SqlReader::class);
69 $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
70 $this->schemaManager = $this->connectionPool->getConnectionForTable($this->tableName)->getSchemaManager();
71 $this->prepareTestTable();
72 }
73
74 /**
75 * Tears down this test suite.
76 */
77 protected function tearDown()
78 {
79 parent::tearDown();
80
81 if ($this->schemaManager->tablesExist([$this->tableName])) {
82 $this->schemaManager->dropTable($this->tableName);
83 }
84 if ($this->schemaManager->tablesExist(['zzz_deleted_' . $this->tableName])) {
85 $this->schemaManager->dropTable('zzz_deleted_' . $this->tableName);
86 }
87 if ($this->schemaManager->tablesExist(['anotherTestTable'])) {
88 $this->schemaManager->dropTable('anotherTestTable');
89 }
90 }
91
92 /**
93 * @test
94 */
95 public function createNewTable()
96 {
97 if ($this->schemaManager->tablesExist([$this->tableName])) {
98 $this->schemaManager->dropTable($this->tableName);
99 }
100
101 $statements = $this->readFixtureFile('newTable');
102 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
103
104 $this->subject->migrate(
105 $statements,
106 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['create_table']
107 );
108
109 $this->assertCount(5, $this->getTableDetails()->getColumns());
110 }
111
112 /**
113 * @test
114 */
115 public function createNewTableIfNotExists()
116 {
117 $statements = $this->readFixtureFile('ifNotExists');
118 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
119
120 $this->subject->migrate(
121 $statements,
122 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['create_table']
123 );
124
125 $this->assertTrue($this->schemaManager->tablesExist(['anotherTestTable']));
126 }
127
128 /**
129 * @test
130 */
131 public function addNewColumns()
132 {
133 $statements = $this->readFixtureFile('addColumnsToTable');
134 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
135
136 $this->subject->migrate(
137 $statements,
138 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
139 );
140
141 $this->assertCount(7, $this->getTableDetails()->getColumns());
142 $this->assertTrue($this->getTableDetails()->hasColumn('title'));
143 $this->assertTrue($this->getTableDetails()->hasColumn('description'));
144 }
145
146 /**
147 * @test
148 */
149 public function changeExistingColumn()
150 {
151 $statements = $this->readFixtureFile('changeExistingColumn');
152 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
153
154 $this->assertInstanceOf(IntegerType::class, $this->getTableDetails()->getColumn('uid')->getType());
155 $this->assertTrue($this->getTableDetails()->getColumn('uid')->getUnsigned());
156
157 $this->subject->migrate(
158 $statements,
159 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']
160 );
161
162 $this->assertInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('uid')->getType());
163 $this->assertFalse($this->getTableDetails()->getColumn('uid')->getUnsigned());
164 }
165
166 /**
167 * @test
168 */
169 public function notNullWithoutDefaultValue()
170 {
171 $statements = $this->readFixtureFile('notNullWithoutDefaultValue');
172 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
173
174 $this->subject->migrate(
175 $statements,
176 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
177 );
178
179 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
180 $this->assertEmpty($updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']);
181 $this->assertTrue($this->getTableDetails()->getColumn('aTestField')->getNotnull());
182 }
183
184 /**
185 * @test
186 */
187 public function defaultNullWithoutNotNull()
188 {
189 $statements = $this->readFixtureFile('defaultNullWithoutNotNull');
190 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
191
192 $this->subject->migrate(
193 $statements,
194 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
195 );
196
197 $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
198 $this->assertEmpty($updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']);
199 $this->assertFalse($this->getTableDetails()->getColumn('aTestField')->getNotnull());
200 $this->assertNull($this->getTableDetails()->getColumn('aTestField')->getDefault());
201 }
202
203 /**
204 * @test
205 */
206 public function renameUnusedField()
207 {
208 $statements = $this->readFixtureFile('unusedColumn');
209 $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
210
211 $this->subject->migrate(
212 $statements,
213 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']
214 );
215
216 $this->assertFalse($this->getTableDetails()->hasColumn('hidden'));
217 $this->assertTrue($this->getTableDetails()->hasColumn('zzz_deleted_hidden'));
218 }
219
220 /**
221 * @test
222 */
223 public function renameUnusedTable()
224 {
225 $statements = $this->readFixtureFile('unusedTable');
226 $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
227
228 $this->subject->migrate(
229 $statements,
230 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change_table']
231 );
232
233 $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
234 $this->assertContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
235 }
236
237 /**
238 * @test
239 */
240 public function dropUnusedField()
241 {
242 $connection = $this->connectionPool->getConnectionForTable($this->tableName);
243 $fromSchema = $this->schemaManager->createSchema();
244 $toSchema = clone $fromSchema;
245 $toSchema->getTable($this->tableName)->addColumn('zzz_deleted_testfield', 'integer');
246 $statements = $fromSchema->getMigrateToSql(
247 $toSchema,
248 $connection->getDatabasePlatform()
249 );
250 $connection->executeUpdate($statements[0]);
251 $this->assertTrue($this->getTableDetails()->hasColumn('zzz_deleted_testfield'));
252
253 $statements = $this->readFixtureFile('newTable');
254 $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
255 $this->subject->migrate(
256 $statements,
257 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['drop']
258 );
259
260 $this->assertFalse($this->getTableDetails()->hasColumn('zzz_deleted_testfield'));
261 }
262
263 /**
264 * @test
265 */
266 public function dropUnusedTable()
267 {
268 $this->schemaManager->renameTable($this->tableName, 'zzz_deleted_' . $this->tableName);
269 $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
270 $this->assertContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
271
272 $statements = $this->readFixtureFile('newTable');
273 $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
274 $this->subject->migrate(
275 $statements,
276 $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['drop_table']
277 );
278
279 $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
280 $this->assertNotContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
281 }
282
283 /**
284 * @test
285 */
286 public function installPerformsOnlyAddAndCreateOperations()
287 {
288 $statements = $this->readFixtureFile('addCreateChange');
289 $this->subject->install($statements, true);
290
291 $this->assertContains('anotherTestTable', $this->schemaManager->listTableNames());
292 $this->assertTrue($this->getTableDetails()->hasColumn('title'));
293 $this->assertTrue($this->getTableDetails()->hasIndex('title'));
294 $this->assertTrue($this->getTableDetails()->getIndex('title')->isUnique());
295 $this->assertNotInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('pid')->getType());
296 }
297
298 /**
299 * @test
300 */
301 public function installCanPerformChangeOperations()
302 {
303 $statements = $this->readFixtureFile('addCreateChange');
304 $this->subject->install($statements);
305
306 $this->assertContains('anotherTestTable', $this->schemaManager->listTableNames());
307 $this->assertTrue($this->getTableDetails()->hasColumn('title'));
308 $this->assertTrue($this->getTableDetails()->hasIndex('title'));
309 $this->assertTrue($this->getTableDetails()->getIndex('title')->isUnique());
310 $this->assertInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('pid')->getType());
311 }
312
313 /**
314 * @test
315 */
316 public function importStaticDataInsertsRecords()
317 {
318 $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', 'importStaticData.sql']));
319 $connection = $this->connectionPool->getConnectionForTable($this->tableName);
320 $statements = $this->sqlReader->getInsertStatementArray($sqlCode);
321 $this->subject->importStaticData($statements);
322
323 $this->assertEquals(2, $connection->count('*', $this->tableName, []));
324 }
325
326 /**
327 * @test
328 */
329 public function importStaticDataIgnoresTableDefinitions()
330 {
331 $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', 'importStaticData.sql']));
332 $statements = $this->sqlReader->getStatementArray($sqlCode);
333 $this->subject->importStaticData($statements);
334
335 $this->assertNotContains('anotherTestTable', $this->schemaManager->listTableNames());
336 }
337
338 /**
339 * Create the base table for all migration tests
340 */
341 protected function prepareTestTable()
342 {
343 $statements = $this->readFixtureFile('newTable');
344 $this->subject->install($statements, true);
345 }
346
347 /**
348 * Helper to return the Doctrine Table object for the test table
349 *
350 * @return \Doctrine\DBAL\Schema\Table
351 */
352 protected function getTableDetails(): Table
353 {
354 return $this->schemaManager->listTableDetails($this->tableName);
355 }
356
357 /**
358 * Helper to read a fixture SQL file and convert it into a statement array.
359 *
360 * @param string $fixtureName
361 * @return array
362 */
363 protected function readFixtureFile(string $fixtureName): array
364 {
365 $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', $fixtureName]) . '.sql');
366
367 return $this->sqlReader->getCreateTableStatementArray($sqlCode);
368 }
369 }