[BUGFIX] SchemaMigrator: Implement platform specific schemas
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / Schema / Parser / TableBuilder.php
1 <?php
2 declare(strict_types=1);
3
4 namespace TYPO3\CMS\Core\Database\Schema\Parser;
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\Platforms\AbstractPlatform;
20 use Doctrine\DBAL\Platforms\MySqlPlatform;
21 use Doctrine\DBAL\Schema\Column;
22 use Doctrine\DBAL\Schema\Index;
23 use Doctrine\DBAL\Schema\Table;
24 use Doctrine\DBAL\Types\Type;
25 use TYPO3\CMS\Core\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
27 use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem;
28 use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem;
29 use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
30 use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
31 use TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName;
32 use TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition;
33 use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
34 use TYPO3\CMS\Core\Database\Schema\Types\SetType;
35 use TYPO3\CMS\Core\Utility\GeneralUtility;
36
37 /**
38 * Converts a CreateTableStatement syntax node into a Doctrine Table
39 * object that represents the table defined in the original SQL statement.
40 */
41 class TableBuilder
42 {
43 /**
44 * @var Table
45 */
46 protected $table;
47
48 /**
49 * @var AbstractPlatform
50 */
51 protected $platform;
52
53 /**
54 * TableBuilder constructor.
55 *
56 * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
57 * @throws \InvalidArgumentException
58 * @throws \Doctrine\DBAL\DBALException
59 */
60 public function __construct(AbstractPlatform $platform = null)
61 {
62 // Register custom data types as no connection might have
63 // been established yet so the types would not be available
64 // when building tables/columns.
65 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
66
67 foreach ($connectionPool->getCustomDoctrineTypes() as $type => $className) {
68 if (!Type::hasType($type)) {
69 Type::addType($type, $className);
70 }
71 }
72 $this->platform = $platform ?: GeneralUtility::makeInstance(MySqlPlatform::class);
73 }
74
75 /**
76 * Create a Doctrine Table object based on the parsed MySQL SQL command.
77 *
78 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement $tableStatement
79 * @return \Doctrine\DBAL\Schema\Table
80 * @throws \Doctrine\DBAL\Schema\SchemaException
81 * @throws \RuntimeException
82 * @throws \InvalidArgumentException
83 */
84 public function create(CreateTableStatement $tableStatement): Table
85 {
86 $this->table = GeneralUtility::makeInstance(
87 Table::class,
88 $tableStatement->tableName->getQuotedName(),
89 [],
90 [],
91 [],
92 0,
93 $this->buildTableOptions($tableStatement->tableOptions)
94 );
95
96 foreach ($tableStatement->createDefinition->items as $item) {
97 switch (get_class($item)) {
98 case CreateColumnDefinitionItem::class:
99 $this->addColumn($item);
100 break;
101 case CreateIndexDefinitionItem::class:
102 $this->addIndex($item);
103 break;
104 case CreateForeignKeyDefinitionItem::class:
105 $this->addForeignKey($item);
106 break;
107 default:
108 throw new \RuntimeException(
109 'Unknown item definition of type "' . get_class($item) . '" encountered.',
110 1472044085
111 );
112 }
113 }
114
115 return $this->table;
116 }
117
118 /**
119 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem $item
120 * @return \Doctrine\DBAL\Schema\Column
121 * @throws \Doctrine\DBAL\Schema\SchemaException
122 * @throws \RuntimeException
123 */
124 protected function addColumn(CreateColumnDefinitionItem $item): Column
125 {
126 $column = $this->table->addColumn(
127 $item->columnName->getQuotedName(),
128 $this->getDoctrineColumnTypeName($item->dataType)
129 );
130
131 $column->setNotnull(!$item->allowNull);
132 $column->setAutoincrement((bool)$item->autoIncrement);
133 $column->setComment($item->comment);
134
135 // Set default value (unless it's an auto increment column)
136 if ($item->hasDefaultValue && !$column->getAutoincrement()) {
137 $column->setDefault($item->defaultValue);
138 }
139
140 if ($item->dataType->getLength()) {
141 $column->setLength($item->dataType->getLength());
142 }
143
144 if ($item->dataType->getPrecision() >= 0) {
145 $column->setPrecision($item->dataType->getPrecision());
146 }
147
148 if ($item->dataType->getScale() >= 0) {
149 $column->setScale($item->dataType->getScale());
150 }
151
152 if ($item->dataType->isUnsigned()) {
153 $column->setUnsigned(true);
154 }
155
156 // Select CHAR/VARCHAR or BINARY/VARBINARY
157 if ($item->dataType->isFixed()) {
158 $column->setFixed(true);
159 }
160
161 if ($item->dataType instanceof DataType\EnumDataType
162 || $item->dataType instanceof DataType\SetDataType
163 ) {
164 $column->setPlatformOption('unquotedValues', $item->dataType->getValues());
165 }
166
167 if ($item->index) {
168 $this->table->addIndex([$item->columnName->getQuotedName()]);
169 }
170
171 if ($item->unique) {
172 $this->table->addUniqueIndex([$item->columnName->getQuotedName()]);
173 }
174
175 if ($item->primary) {
176 $this->table->setPrimaryKey([$item->columnName->getQuotedName()]);
177 }
178
179 if ($item->reference !== null) {
180 $this->addForeignKeyConstraint(
181 [$item->columnName->getQuotedName()],
182 $item->reference
183 );
184 }
185
186 return $column;
187 }
188
189 /**
190 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem $item
191 * @return \Doctrine\DBAL\Schema\Index
192 * @throws \Doctrine\DBAL\Schema\SchemaException
193 * @throws \InvalidArgumentException
194 */
195 protected function addIndex(CreateIndexDefinitionItem $item): Index
196 {
197 $indexName = $item->indexName->getQuotedName();
198
199 $columnNames = array_map(
200 function (IndexColumnName $columnName) {
201 if ($columnName->length) {
202 return $columnName->columnName->getQuotedName() . '(' . $columnName->length . ')';
203 }
204 return $columnName->columnName->getQuotedName();
205 },
206 $item->columnNames
207 );
208
209 if ($item->isPrimary) {
210 $this->table->setPrimaryKey($columnNames);
211 $index = $this->table->getPrimaryKey();
212 } else {
213 $index = GeneralUtility::makeInstance(
214 Index::class,
215 $indexName,
216 $columnNames,
217 $item->isUnique,
218 $item->isPrimary
219 );
220
221 if ($item->isFulltext) {
222 $index->addFlag('fulltext');
223 } elseif ($item->isSpatial) {
224 $index->addFlag('spatial');
225 }
226
227 $this->table = GeneralUtility::makeInstance(
228 Table::class,
229 $this->table->getQuotedName($this->platform),
230 $this->table->getColumns(),
231 array_merge($this->table->getIndexes(), [strtolower($indexName) => $index]),
232 $this->table->getForeignKeys(),
233 0,
234 $this->table->getOptions()
235 );
236 }
237
238 return $index;
239 }
240
241 /**
242 * Prepare a explicit foreign key definition item to be added to the table being built.
243 *
244 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem $item
245 */
246 protected function addForeignKey(CreateForeignKeyDefinitionItem $item)
247 {
248 $indexName = $item->indexName->getQuotedName() ?: null;
249 $localColumnNames = array_map(
250 function (IndexColumnName $columnName) {
251 return $columnName->columnName->getQuotedName();
252 },
253 $item->columnNames
254 );
255 $this->addForeignKeyConstraint($localColumnNames, $item->reference, $indexName);
256 }
257
258 /**
259 * Add a foreign key constraint to the table being built.
260 *
261 * @param string[] $localColumnNames
262 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition $referenceDefinition
263 * @param string $indexName
264 */
265 protected function addForeignKeyConstraint(
266 array $localColumnNames,
267 ReferenceDefinition $referenceDefinition,
268 string $indexName = null
269 ) {
270 $foreignTableName = $referenceDefinition->tableName->getQuotedName();
271 $foreignColumNames = array_map(
272 function (IndexColumnName $columnName) {
273 return $columnName->columnName->getQuotedName();
274 },
275 $referenceDefinition->columnNames
276 );
277
278 $options = [
279 'onDelete' => $referenceDefinition->onDelete,
280 'onUpdate' => $referenceDefinition->onUpdate,
281 ];
282
283 $this->table->addForeignKeyConstraint(
284 $foreignTableName,
285 $localColumnNames,
286 $foreignColumNames,
287 $options,
288 $indexName
289 );
290 }
291
292 /**
293 * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType $dataType
294 * @return string
295 * @throws \RuntimeException
296 */
297 protected function getDoctrineColumnTypeName(DataType\AbstractDataType $dataType): string
298 {
299 $doctrineType = null;
300 switch (get_class($dataType)) {
301 case DataType\TinyIntDataType::class:
302 // TINYINT is MySQL specific and mapped to a standard SMALLINT
303 case DataType\SmallIntDataType::class:
304 $doctrineType = Type::SMALLINT;
305 break;
306 case DataType\MediumIntDataType::class:
307 // MEDIUMINT is MySQL specific and mapped to a standard INT
308 case DataType\IntegerDataType::class:
309 $doctrineType = Type::INTEGER;
310 break;
311 case DataType\BigIntDataType::class:
312 $doctrineType = Type::BIGINT;
313 break;
314 case DataType\BinaryDataType::class:
315 case DataType\VarBinaryDataType::class:
316 // CHAR/VARCHAR is determined by "fixed" column property
317 $doctrineType = Type::BINARY;
318 break;
319 case DataType\TinyBlobDataType::class:
320 case DataType\MediumBlobDataType::class:
321 case DataType\BlobDataType::class:
322 case DataType\LongBlobDataType::class:
323 // Actual field type is determined by field length
324 $doctrineType = Type::BLOB;
325 break;
326 case DataType\DateDataType::class:
327 $doctrineType = Type::DATE;
328 break;
329 case DataType\TimestampDataType::class:
330 case DataType\DateTimeDataType::class:
331 // TIMESTAMP or DATETIME are determined by "version" column property
332 $doctrineType = Type::DATETIME;
333 break;
334 case DataType\NumericDataType::class:
335 case DataType\DecimalDataType::class:
336 $doctrineType = Type::DECIMAL;
337 break;
338 case DataType\RealDataType::class:
339 case DataType\FloatDataType::class:
340 case DataType\DoubleDataType::class:
341 $doctrineType = Type::FLOAT;
342 break;
343 case DataType\TimeDataType::class:
344 $doctrineType = Type::TIME;
345 break;
346 case DataType\TinyTextDataType::class:
347 case DataType\MediumTextDataType::class:
348 case DataType\TextDataType::class:
349 case DataType\LongTextDataType::class:
350 $doctrineType = Type::TEXT;
351 break;
352 case DataType\CharDataType::class:
353 case DataType\VarCharDataType::class:
354 $doctrineType = Type::STRING;
355 break;
356 case DataType\EnumDataType::class:
357 $doctrineType = EnumType::TYPE;
358 break;
359 case DataType\SetDataType::class:
360 $doctrineType = SetType::TYPE;
361 break;
362 case DataType\JsonDataType::class:
363 // JSON is not supported in Doctrine 2.5, mapping to the more generic TEXT type
364 $doctrineType = SetType::TEXT;
365 break;
366 case DataType\YearDataType::class:
367 // The YEAR data type is MySQL specific and offers little to no benefit.
368 // The two-digit year logic implemented in this data type (1-69 mapped to
369 // 2001-2069, 70-99 mapped to 1970-1999) can be easily implemented in the
370 // application and for all other accounts it's an integer with a valid
371 // range of 1901 to 2155.
372 // Using a SMALLINT covers the value range and ensures database compatibility.
373 $doctrineType = SetType::SMALLINT;
374 break;
375 default:
376 throw new \RuntimeException(
377 'Unsupported data type: ' . get_class($dataType) . '!',
378 1472046376
379 );
380 }
381
382 return $doctrineType;
383 }
384
385 /**
386 * Build the table specific options as far as they are supported by Doctrine.
387 *
388 * @param array $tableOptions
389 * @return array
390 */
391 protected function buildTableOptions(array $tableOptions): array
392 {
393 $options = [];
394
395 if (!empty($tableOptions['engine'])) {
396 $options['engine'] = (string)$tableOptions['engine'];
397 }
398 if (!empty($tableOptions['character_set'])) {
399 $options['charset'] = (string)$tableOptions['character_set'];
400 }
401 if (!empty($tableOptions['collation'])) {
402 $options['collate'] = (string)$tableOptions['collation'];
403 }
404 if (!empty($tableOptions['auto_increment'])) {
405 $options['auto_increment'] = (string)$tableOptions['auto_increment'];
406 }
407 if (!empty($tableOptions['comment'])) {
408 $options['comment'] = (string)$tableOptions['comment'];
409 }
410 if (!empty($tableOptions['row_format'])) {
411 $options['row_format'] = (string)$tableOptions['row_format'];
412 }
413
414 return $options;
415 }
416 }