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