TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Doctrine\DBAL\Schema\Comparator as DoctrineComparator;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\TableDiff as DoctrineTableDiff;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* Compares two Schemas and returns an instance of SchemaDiff.
*
* @internal not part of public core API.
*/
final readonly class Comparator
{
public function __construct(
private DoctrineComparator $comparator
) {}
public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff
{
$schemaDiff = $this->comparator->compareSchemas($oldSchema, $newSchema);
$alteredTables = $this->mapAlteredTablesToTypo3TableDiff($schemaDiff->getAlteredTables());
$alteredTables = SchemaDiff::ensureCollection(...$alteredTables);
$alteredTables = $this->compareTableOptions($oldSchema, $newSchema, $alteredTables);
return SchemaDiff::ensure(
$schemaDiff,
[
'alteredTables' => $alteredTables,
]
);
}
/**
* @param array<DoctrineTableDiff> $alteredTables
* @return array<TableDiff>
*/
private function mapAlteredTablesToTypo3TableDiff(array $alteredTables): array
{
return array_map(
static fn(DoctrineTableDiff $tableDiff): TableDiff => TableDiff::ensure($tableDiff),
$alteredTables
);
}
/**
* Provide change information about table options like the ENGINE (#77786)
* which are not implemented by doctrine/dbal itself
*
* @param array<string, TableDiff> $alteredTables
* @return array<string, TableDiff>
*/
private function compareTableOptions(Schema $oldSchema, Schema $newSchema, array $alteredTables): array
{
foreach ($newSchema->getTables() as $newTable) {
$newTableName = $newTable->getShortestName($newSchema->getName());
if (!$oldSchema->hasTable($newTableName)) {
// new table, no ALTER TABLE needed
continue;
}
$oldTable = $oldSchema->getTable($newTableName);
$newTableOptions = array_merge($oldTable->getOptions(), $newTable->getOptions());
$optionDiff = ArrayUtility::arrayDiffAssocRecursive($newTableOptions, $oldTable->getOptions());
if ($optionDiff === []) {
continue;
}
$key = $newTable->getName();
$tableDiff = $alteredTables[$key] ?? new TableDiff($newTable);
$tableDiff->setTableOptions($optionDiff);
$alteredTables[$key] = $tableDiff;
}
return $alteredTables;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Exception;
use TYPO3\CMS\Core\Exception;
/**
* A detail exception thrown within DefaultTcaSchema.
*
* @internal not part of public core API.
*/
class DefaultTcaSchemaTablePositionException extends Exception {}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Exception;
/**
* @internal not part of public core API.
*/
class StatementException extends \Exception
{
/**
* @internal
*/
public static function sqlError(string $sql): StatementException
{
return new self($sql, 1471504820);
}
/**
* @internal
*/
public static function syntaxError(string $message, ?\Exception $previous = null): StatementException
{
return new self('[SQL Error] ' . $message, 1471504821, $previous);
}
/**
* @internal
*/
public static function semanticError(string $message, ?\Exception $previous = null): StatementException
{
return new self('[Semantic Error] ' . $message, 1471504822, $previous);
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Information;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\Exception\TypesException;
use Doctrine\DBAL\Types\Type;
use TYPO3\CMS\Core\Database\Schema\SchemaInformation;
/**
* Provides subset of column schema information compared to {@see Column} and intended to be cacheable.
*
* @internal This class is only for internal core usage and is not part of the public core API.
*/
final readonly class ColumnInfo
{
/**
* @param string[] $values
*/
public function __construct(
public string $name,
public string $typeName,
public mixed $default,
public bool $notNull,
public ?int $length,
public ?int $precision,
public int $scale,
public bool $fixed,
public bool $unsigned,
public bool $autoincrement,
public array $values,
) {}
/**
* @throws TypesException
*/
public function getType(): Type
{
return Type::getType($this->typeName);
}
/**
* Used in {@see SchemaInformation::buildTableInformation()} to transform doctrine Columns to ColumnInfo.
*/
public static function convertFromDoctrineColumn(Column $column): self
{
// `Column->getType()` is not passed here by intention to mitigate cache issues getting information from
// persisted cache due to `sbl_object_id()` usage in the Doctrine DBAL TypesRegistry not matching the
// type later on. Skipping it here and not having it as class property is part of the mitigation strategy
// and resolves the cache issues with `Column` directly.
return new self(
name: $column->getName(),
typeName: Type::lookupName($column->getType()),
default: $column->getDefault(),
notNull: $column->getNotnull(),
length: $column->getLength(),
precision: $column->getPrecision(),
scale: $column->getScale(),
fixed: $column->getFixed(),
unsigned: $column->getUnsigned(),
autoincrement: $column->getAutoincrement(),
values: $column->getValues(),
);
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Information;
use Doctrine\DBAL\Schema\Table;
/**
* Provides reduced table information compared to {@see Table} and intended to be cacheable.
*
* @internal This class is only for internal core usage and is not part of the public core API.
*/
final readonly class TableInfo
{
/**
* @param array<string, ColumnInfo> $columnInfos
*/
public function __construct(
private string $name,
private array $columnInfos,
) {}
public function getName(): string
{
return $this->name;
}
public function hasColumnInfo(string $columnName): bool
{
return in_array($columnName, $this->getColumnNames(), true);
}
public function getColumnInfo(string $columnName): ?ColumnInfo
{
return $this->columnInfos[$columnName] ?? null;
}
public function getColumnNames(): array
{
return array_keys($this->columnInfos);
}
/**
* @return array<string, ColumnInfo>
*/
public function getColumnInfos(): array
{
return $this->columnInfos;
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Base class for all definition items that can occur in the definition
* of a table, namely fields, indexes and foreign keys.
*/
abstract class AbstractCreateDefinitionItem {}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Base class for all create type statements like CREATE TABLE
* or CREATE VIEW.
*/
abstract class AbstractCreateStatement {}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType;
/**
* Syntax tree node for column definitions within "create table" statements.
* Holds basic attributes common to all types of columns.
*
* @internal
*/
final class CreateColumnDefinitionItem extends AbstractCreateDefinitionItem
{
public bool $allowNull = true;
// Has explicit default value?
public bool $hasDefaultValue = false;
// The explicit default value
public mixed $defaultValue = null;
public bool $autoIncrement = false;
// Create non-unique index for column?
public bool $index = false;
// Create unique constraint for column?
public bool $unique = false;
// Use column as primary key for table?
public bool $primary = false;
public ?string $comment = null;
// Column format: "dynamic" or "fixed"
public ?string $columnFormat = null;
// The storage type for the column (ignored unless MySQL Cluster with NDB Engine)
public ?string $storage = null;
public ?ReferenceDefinition $reference = null;
public function __construct(
public readonly Identifier $columnName,
public readonly AbstractDataType $dataType,
) {}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node for the whole definition of a table/view. Collects
* the nodes for fields, indexes and foreign keys.
*
* @internal
*/
final readonly class CreateDefinition
{
/**
* @param AbstractCreateDefinitionItem[] $items
*/
public function __construct(
public array $items
) {}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to structure a foreign key definition.
*
* @internal
*/
final class CreateForeignKeyDefinitionItem extends AbstractCreateDefinitionItem
{
/**
* @param IndexColumnName[] $columnNames
*/
public function __construct(
public readonly Identifier $indexName,
public readonly array $columnNames,
public readonly ReferenceDefinition $reference,
) {}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to structure an index definition.
*
* @internal
*/
final class CreateIndexDefinitionItem extends AbstractCreateDefinitionItem
{
// Use a special index type (MySQL: BTREE | HASH)
public string $indexType = '';
// @var IndexColumnName[]
public array $columnNames = [];
// Index options KEY_BLOCK_SIZE, USING, WITH PARSER or COMMENT
public array $options = [];
public function __construct(
public readonly ?Identifier $indexName = null,
public readonly bool $isPrimary = false,
public readonly bool $isUnique = false,
public readonly bool $isSpatial = false,
public readonly bool $isFulltext = false
) {}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to represent the initial CREATE TABLE statement in the
* syntax tree. Represents everything up to the start of the definition
* of fields/indexes/foreign keys.
*
* @internal
*/
final class CreateTableClause
{
public function __construct(
public readonly Identifier $tableName,
public bool $isTemporary = false
) {}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Root node for a CREATE TABLE statement in the syntax tree.
*
* @internal
*/
final class CreateTableStatement extends AbstractCreateStatement
{
public Identifier $tableName;
public bool $isTemporary = false;
public array $tableOptions = [];
public function __construct(
CreateTableClause $createTableClause,
public readonly CreateDefinition $createDefinition
) {
$this->tableName = $createTableClause->tableName;
$this->isTemporary = $createTableClause->isTemporary;
}
}
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Base class for all data types that contains properties
* common to all data types.
*
* @internal
*/
abstract class AbstractDataType
{
/** Used by most field types for length/precision information */
protected int $length = 0;
/** Used for floating point type columns. -1 is used to indicate no value has been set. */
protected int $precision = -1;
/** Used for floating point type columns. -1 is used to indicate that no value has been set. */
protected int $scale = -1;
/** Differentiate between CHAR/VARCHAR and BINARY/VARBINARY */
protected bool $fixed = false;
/** Unsigned flag for numeric columns */
protected bool $unsigned = false;
/** Extra options for a column that control specific features/flags */
protected array $options = [];
/** Options for ENUM/SET data types */
protected array $values = [];
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length): void
{
$this->length = $length;
}
public function getPrecision(): int
{
return $this->precision;
}
public function setPrecision(int $precision): void
{
$this->precision = $precision;
}
public function getScale(): int
{
return $this->scale;
}
public function setScale(int $scale): void
{
$this->scale = $scale;
}
public function isFixed(): bool
{
return $this->fixed;
}
public function setFixed(bool $fixed): void
{
$this->fixed = $fixed;
}
public function getOptions(): array
{
return $this->options;
}
public function setOptions(array $options): void
{
$this->options = $options;
}
public function isUnsigned(): bool
{
return $this->unsigned;
}
public function setUnsigned(bool $unsigned): void
{
$this->unsigned = $unsigned;
}
public function getValues(): array
{
return $this->values;
}
public function setValues(array $values): void
{
$this->values = $values;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the BIGINT SQL column type
*
* @internal
*/
final class BigIntDataType extends IntegerDataType {}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the BINARY SQL column type
*
* @internal
*/
final class BinaryDataType extends AbstractDataType
{
public function __construct(int $length)
{
/**
* BINARY is "fixed type". Setting it here instructs Doctrine DBAL to use this type instead of
* the "variable type" when being transformed within the {@see TableBuilder::addColumn()} method.
*/
$this->fixed = true;
$this->length = $length;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the BIT SQL column type
*
* @internal
*/
final class BitDataType extends AbstractDataType
{
public function __construct(int $length)
{
$this->length = $length;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the BLOB SQL column type
*
* @internal
*/
class BlobDataType extends AbstractDataType
{
public function __construct()
{
// MySQL BLOB can store 64KB
$this->length = 65535;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the CHAR SQL column type
*
* @internal
*/
final class CharDataType extends AbstractDataType
{
public function __construct(int $length, array $options)
{
/**
* CHAR is "fixed type". Setting it here instructs Doctrine DBAL to use this type instead of
* the "variable type" when being transformed within the {@see TableBuilder::addColumn()} method.
*/
$this->fixed = true;
$this->length = $length;
$this->options = $options;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the DATE SQL column type
*
* @internal
*/
final class DateDataType extends AbstractDataType {}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the DATETIME SQL column type
*
* @internal
*/
final class DateTimeDataType extends AbstractDataType
{
public function __construct(int $length)
{
$this->length = $length;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the DECIMAL SQL column type
*
* @internal
*/
class DecimalDataType extends AbstractDataType
{
public function __construct(array $dataTypeDecimals, array $dataTypeOptions)
{
$this->precision = $dataTypeDecimals['length'] ?? -1;
$this->scale = $dataTypeDecimals['decimals'] ?? -1;
$this->options = $dataTypeOptions;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the DOUBLE SQL column type
*
* @internal
*/
final class DoubleDataType extends FloatDataType {}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the ENUM SQL column type
*
* @internal
*/
final class EnumDataType extends AbstractDataType
{
public function __construct(array $values, array $options)
{
$this->values = $values;
$this->options = $options;
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the FLOAT SQL column type
*
* @internal
*/
class FloatDataType extends AbstractDataType
{
public function __construct(array $dataTypeDecimals, array $dataTypeOptions)
{
// -1 is used to indicate that no value has been provided
$this->precision = $dataTypeDecimals['length'] ?? -1;
$this->scale = $dataTypeDecimals['decimals'] ?? -1;
$this->options = $dataTypeOptions;
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the INT SQL column type
*
* @internal
*/
class IntegerDataType extends AbstractDataType
{
public function __construct(int $length, array $options)
{
$this->length = $length;
$this->options = $options;
if (array_key_exists('unsigned', $options) && $options['unsigned']) {
$this->setUnsigned(true);
}
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the JSON SQL column type
*
* @internal
*/
final class JsonDataType extends AbstractDataType
{
public function __construct()
{
// JSON is not yet supported by Doctrine 2.5 and will be remapped
// to a TEXT type. Setting the length here will ensure a LONGTEXT
// column type is selected.
$this->length = 2147483647;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the LONGBLOB SQL column type
*
* @internal
*/
final class LongBlobDataType extends BlobDataType
{
public function __construct()
{
parent::__construct();
// MySQL LONGBLOB can store 4GB of data, to be 32bit safe only claim 2GB
$this->length = 2147483647;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the LONGTEXT SQL column type
*
* @internal
*/
final class LongTextDataType extends TextDataType
{
public function __construct(array $options)
{
parent::__construct($options);
// MySQL LONGTEXT can store 4GB of data, to be 32bit safe only claim 2GB
$this->length = 2147483647;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the MEDIUMBLOB SQL column type
*
* @internal
*/
final class MediumBlobDataType extends BlobDataType
{
public function __construct()
{
parent::__construct();
// MySQL MEDIUMBLOB can store 16MB
$this->length = 16777215;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the MEDIUMINT SQL column type
*
* @internal
*/
final class MediumIntDataType extends IntegerDataType {}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the MEDIUMTEXT SQL column type
*
* @internal
*/
final class MediumTextDataType extends TextDataType
{
public function __construct(array $options)
{
parent::__construct($options);
// MySQL MEDIUMTEXT can store 16MB
$this->length = 16777215;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the NUMERIC SQL column type
*
* @internal
*/
final class NumericDataType extends DecimalDataType {}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the REAL SQL column type
*
* @internal
*/
final class RealDataType extends FloatDataType {}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the SET SQL column type
*
* @internal
*/
final class SetDataType extends AbstractDataType
{
public function __construct(array $values, array $options)
{
$this->values = $values;
$this->options = $options;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the SMALLINT SQL column type
*
* @internal
*/
final class SmallIntDataType extends IntegerDataType {}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TEXT SQL column type
*
* @internal
*/
class TextDataType extends AbstractDataType
{
public function __construct(array $options)
{
// MySQL TEXT can store 64KB
$this->length = 65535;
$this->options = $options;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TIME SQL column type
*
* @internal
*/
final class TimeDataType extends AbstractDataType
{
public function __construct(int $length)
{
$this->length = $length;
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TIMESTAMP SQL column type
*
* @internal
*/
final class TimestampDataType extends AbstractDataType
{
public function __construct(int $length)
{
$this->length = $length;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TINYBLOB SQL column type
*
* @internal
*/
final class TinyBlobDataType extends BlobDataType
{
public function __construct()
{
parent::__construct();
// MySQL TINYBLOB can store 255 bytes
$this->length = 255;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TINYINT SQL column type
*
* @internal
*/
final class TinyIntDataType extends IntegerDataType {}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the TINYTEXT SQL column type
*
* @internal
*/
final class TinyTextDataType extends TextDataType
{
public function __construct(array $options)
{
parent::__construct($options);
// MySQL TINYTEXT can store 255 characters
$this->length = 255;
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* @internal for `EXT:core` internal usage and not part of public API.
*/
final class UuidDataType extends AbstractDataType {}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the VARBINARY SQL column type
*
* @internal
*/
final class VarBinaryDataType extends AbstractDataType
{
public function __construct(int $length)
{
$this->length = $length;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the VARCHAR SQL column type
*
* @internal
*/
final class VarCharDataType extends AbstractDataType
{
public function __construct(int $length, array $options)
{
$this->length = $length;
$this->options = $options;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
/**
* Node representing the YEAR SQL column type
*
* @internal
*/
final class YearDataType extends AbstractDataType {}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to represent identifiers used in various parts of a
* SQL statements like table, field or index names.
*
* @internal
*/
final class Identifier
{
private string $quoteChar = '`';
public function __construct(
public readonly string $schemaObjectName
) {}
/**
* Quotes the schema object name.
*/
public function getQuotedName(): string
{
$c = $this->quoteChar;
return $c . str_replace($c, $c . $c, $this->schemaObjectName) . $c;
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to represent a column within an index, which can in MySQL
* context consist of the actual column name, length information for a partial
* index and a direction which influences default sorting and access patterns.
*
* @internal
*/
final readonly class IndexColumnName
{
public function __construct(
public Identifier $columnName,
public int $length,
public ?string $direction = null
) {}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
/**
* Syntax node to represent the REFERENCES part of a foreign key
* definition, encapsulating ON UPDATE/ON DELETE actions as well
* as the foreign table name and columns.
*
* @internal
*/
final class ReferenceDefinition
{
// Match type if given: FULL, PARTIAL or SIMPLE
public ?string $match = null;
// Reference option if given: RESTRICT | CASCADE | SET NULL | NO ACTION
public ?string $onDelete = null;
// Reference option if given: RESTRICT | CASCADE | SET NULL | NO ACTION
public ?string $onUpdate = null;
/**
* @param IndexColumnName[] $columnNames
*/
public function __construct(
public readonly Identifier $tableName,
public readonly array $columnNames,
) {}
}
+266
View File
@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser;
use Doctrine\Common\Lexer\AbstractLexer;
/**
* Scans a MySQL CREATE TABLE statement for tokens.
*/
class Lexer extends AbstractLexer
{
// All tokens that are not valid identifiers must be < 100
public const T_NONE = 1;
public const T_STRING = 2;
public const T_INPUT_PARAMETER = 3;
public const T_CLOSE_PARENTHESIS = 4;
public const T_OPEN_PARENTHESIS = 5;
public const T_COMMA = 6;
public const T_DIVIDE = 7;
public const T_DOT = 8;
public const T_EQUALS = 9;
public const T_GREATER_THAN = 10;
public const T_LOWER_THAN = 11;
public const T_MINUS = 12;
public const T_MULTIPLY = 13;
public const T_NEGATE = 14;
public const T_PLUS = 15;
public const T_OPEN_CURLY_BRACE = 16;
public const T_CLOSE_CURLY_BRACE = 17;
public const T_SEMICOLON = 18;
// All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100
public const T_IDENTIFIER = 100;
// All tokens that could be considered as a data type should be >= 200
public const T_BIT = 201;
public const T_TINYINT = 202;
public const T_SMALLINT = 203;
public const T_MEDIUMINT = 204;
public const T_INT = 205;
public const T_INTEGER = 206;
public const T_BIGINT = 207;
public const T_REAL = 208;
public const T_DOUBLE = 209;
public const T_FLOAT = 210;
public const T_DECIMAL = 211;
public const T_NUMERIC = 212;
public const T_DATE = 213;
public const T_TIME = 214;
public const T_TIMESTAMP = 215;
public const T_DATETIME = 216;
public const T_YEAR = 217;
public const T_CHAR = 218;
public const T_VARCHAR = 219;
public const T_BINARY = 220;
public const T_VARBINARY = 221;
public const T_TINYBLOB = 222;
public const T_BLOB = 223;
public const T_MEDIUMBLOB = 224;
public const T_LONGBLOB = 225;
public const T_TINYTEXT = 226;
public const T_TEXT = 227;
public const T_MEDIUMTEXT = 228;
public const T_LONGTEXT = 229;
public const T_ENUM = 230;
public const T_SET = 231;
public const T_JSON = 232;
public const T_UUID = 233;
// All keyword tokens should be >= 300
public const T_CREATE = 300;
public const T_TEMPORARY = 301;
public const T_TABLE = 302;
public const T_IF = 303;
public const T_NOT = 304;
public const T_EXISTS = 305;
public const T_CONSTRAINT = 306;
public const T_INDEX = 307;
public const T_KEY = 308;
public const T_FULLTEXT = 309;
public const T_SPATIAL = 310;
public const T_PRIMARY = 311;
public const T_UNIQUE = 312;
public const T_CHECK = 313;
public const T_DEFAULT = 314;
public const T_AUTO_INCREMENT = 315;
public const T_COMMENT = 316;
public const T_COLUMN_FORMAT = 317;
public const T_STORAGE = 318;
public const T_REFERENCES = 319;
public const T_NULL = 320;
public const T_FIXED = 321;
public const T_DYNAMIC = 322;
public const T_MEMORY = 323;
public const T_DISK = 324;
public const T_UNSIGNED = 325;
public const T_ZEROFILL = 326;
public const T_CURRENT_TIMESTAMP = 327;
public const T_CHARACTER = 328;
public const T_COLLATE = 329;
public const T_ASC = 330;
public const T_DESC = 331;
public const T_MATCH = 332;
public const T_FULL = 333;
public const T_PARTIAL = 334;
public const T_SIMPLE = 335;
public const T_ON = 336;
public const T_UPDATE = 337;
public const T_DELETE = 338;
public const T_RESTRICT = 339;
public const T_CASCADE = 340;
public const T_NO = 341;
public const T_ACTION = 342;
public const T_USING = 343;
public const T_BTREE = 344;
public const T_HASH = 345;
public const T_KEY_BLOCK_SIZE = 346;
public const T_WITH = 347;
public const T_PARSER = 348;
public const T_FOREIGN = 349;
public const T_ENGINE = 350;
public const T_AVG_ROW_LENGTH = 351;
public const T_CHECKSUM = 352;
public const T_COMPRESSION = 353;
public const T_CONNECTION = 354;
public const T_DATA = 355;
public const T_DIRECTORY = 356;
public const T_DELAY_KEY_WRITE = 357;
public const T_ENCRYPTION = 358;
public const T_INSERT_METHOD = 359;
public const T_MAX_ROWS = 360;
public const T_MIN_ROWS = 361;
public const T_PACK_KEYS = 362;
public const T_PASSWORD = 363;
public const T_ROW_FORMAT = 364;
public const T_STATS_AUTO_RECALC = 365;
public const T_STATS_PERSISTENT = 366;
public const T_STATS_SAMPLE_PAGES = 367;
public const T_TABLESPACE = 368;
public const T_UNION = 369;
public const T_PRECISION = 370;
/**
* Lexical catchable patterns.
*/
protected function getCatchablePatterns(): array
{
return [
'(?:-?[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers
'`(?:[^`]|``)*`', // quoted identifiers
"'(?:[^']|'')*'", // quoted strings
'\)', // closing parenthesis
'[a-z0-9$_][\w$]*', // unquoted identifiers
];
}
/**
* Lexical non-catchable patterns.
*/
protected function getNonCatchablePatterns(): array
{
return ['\s+'];
}
/**
* Retrieve token type. Also processes the token value if necessary.
*
* @param string $value
*/
protected function getType(&$value): int
{
$type = self::T_NONE;
// Recognize numeric values
if (is_numeric($value)) {
if (str_contains($value, '.') || stripos($value, 'e') !== false) {
return self::T_FLOAT;
}
return self::T_INTEGER;
}
// Recognize quoted strings
if ($value[0] === "'") {
$value = str_replace("''", "'", substr($value, 1, -1));
return self::T_STRING;
}
// Recognize quoted strings
if ($value[0] === '`') {
$value = str_replace('``', '`', substr($value, 1, -1));
return self::T_IDENTIFIER;
}
// Recognize identifiers, aliased or qualified names
if (ctype_alpha($value[0])) {
$name = 'TYPO3\\CMS\\Core\\Database\\Schema\\Parser\\Lexer::T_' . strtoupper($value);
if (defined($name)) {
$type = constant($name);
if ($type > 100) {
return $type;
}
}
return self::T_STRING;
}
switch ($value) {
// Recognize symbols
case '.':
return self::T_DOT;
case ';':
return self::T_SEMICOLON;
case ',':
return self::T_COMMA;
case '(':
return self::T_OPEN_PARENTHESIS;
case ')':
return self::T_CLOSE_PARENTHESIS;
case '=':
return self::T_EQUALS;
case '>':
return self::T_GREATER_THAN;
case '<':
return self::T_LOWER_THAN;
case '+':
return self::T_PLUS;
case '-':
return self::T_MINUS;
case '*':
return self::T_MULTIPLY;
case '/':
return self::T_DIVIDE;
case '!':
return self::T_NEGATE;
case '{':
return self::T_OPEN_CURLY_BRACE;
case '}':
return self::T_CLOSE_CURLY_BRACE;
// Default
default:
// Do nothing
}
return $type;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Parser;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BigIntDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BinaryDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BlobDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\CharDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateTimeDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DecimalDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DoubleDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\EnumDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\FloatDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\IntegerDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\JsonDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\LongBlobDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\LongTextDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumBlobDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumIntDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumTextDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\NumericDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\RealDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SetDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SmallIntDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TextDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimeDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimestampDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyBlobDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyIntDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyTextDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\UuidDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarBinaryDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarCharDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\YearDataType;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName;
use TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition;
use TYPO3\CMS\Core\Database\Schema\Types\SetType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Converts a CreateTableStatement syntax node into a Doctrine Table
* object that represents the table defined in the original SQL statement.
*/
class TableBuilder
{
/**
* @var Table
*/
protected $table;
/**
* @var AbstractPlatform
*/
protected $platform;
/**
* TableBuilder constructor.
*
* @throws \InvalidArgumentException
* @throws \Doctrine\DBAL\Exception
*/
public function __construct(?AbstractPlatform $platform = null)
{
// Register custom data types as no connection might have
// been established yet so the types would not be available
// when building tables/columns.
ConnectionPool::registerDoctrineTypes();
$this->platform = $platform ?: GeneralUtility::makeInstance(MySQLPlatform::class);
}
/**
* Create a Doctrine Table object based on the parsed MySQL SQL command.
*
* @throws \Doctrine\DBAL\Schema\SchemaException
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
public function create(CreateTableStatement $tableStatement): Table
{
$this->table = GeneralUtility::makeInstance(
Table::class,
$tableStatement->tableName->getQuotedName(),
[],
[],
[],
[],
$this->buildTableOptions($tableStatement->tableOptions)
);
foreach ($tableStatement->createDefinition->items as $item) {
switch (get_class($item)) {
case CreateColumnDefinitionItem::class:
$this->addColumn($item);
break;
case CreateIndexDefinitionItem::class:
$this->addIndex($item);
break;
case CreateForeignKeyDefinitionItem::class:
$this->addForeignKey($item);
break;
default:
throw new \RuntimeException(
'Unknown item definition of type "' . get_class($item) . '" encountered.',
1472044085
);
}
}
return $this->table;
}
/**
* @throws \Doctrine\DBAL\Schema\SchemaException
* @throws \RuntimeException
*/
protected function addColumn(CreateColumnDefinitionItem $item): Column
{
$column = $this->table->addColumn(
$item->columnName->getQuotedName(),
$this->getDoctrineColumnTypeName($item->dataType)
);
$column->setNotnull($item->allowNull === false);
$column->setAutoincrement($item->autoIncrement);
$column->setComment((string)$item->comment);
// Set default value (unless it's an auto increment column)
if ($item->hasDefaultValue && !$column->getAutoincrement()) {
$column->setDefault($item->defaultValue);
}
if ($item->dataType->getLength()) {
$column->setLength($item->dataType->getLength());
}
if ($item->dataType->getPrecision() >= 0) {
$column->setPrecision($item->dataType->getPrecision());
}
if ($item->dataType->getScale() >= 0) {
$column->setScale($item->dataType->getScale());
}
if ($item->dataType->isUnsigned()) {
$column->setUnsigned(true);
}
// Select CHAR/VARCHAR or BINARY/VARBINARY
if ($item->dataType->isFixed()) {
$column->setFixed(true);
}
if ($item->dataType instanceof SetDataType) {
$column->setValues($item->dataType->getValues());
}
if ($item->dataType instanceof EnumDataType) {
$column->setValues($item->dataType->getValues());
}
$dataTypeSupportsCharsetAndCollation = (
$item->dataType instanceof CharDataType
|| $item->dataType instanceof VarCharDataType
|| $item->dataType instanceof TextDataType
);
$options = $item->dataType->getOptions();
if ($dataTypeSupportsCharsetAndCollation && ($options['charset'] ?? null)) {
$column->setPlatformOption('charset', $options['charset']);
}
if ($dataTypeSupportsCharsetAndCollation && ($options['charset'] ?? null)) {
$column->setPlatformOption('collation', $options['collation']);
}
if ($item->index) {
$this->table->addIndex([$item->columnName->getQuotedName()]);
}
if ($item->unique) {
$this->table->addUniqueIndex([$item->columnName->getQuotedName()]);
}
if ($item->primary) {
$this->table->setPrimaryKey([$item->columnName->getQuotedName()]);
}
if ($item->reference !== null) {
$this->addForeignKeyConstraint(
[$item->columnName->getQuotedName()],
$item->reference
);
}
return $column;
}
/**
* @throws \Doctrine\DBAL\Schema\SchemaException
* @throws \InvalidArgumentException
*/
protected function addIndex(CreateIndexDefinitionItem $item): Index
{
$indexName = $item->indexName->getQuotedName();
$columnNames = array_map(
static function (IndexColumnName $columnName): string {
if ($columnName->length) {
return $columnName->columnName->getQuotedName() . '(' . $columnName->length . ')';
}
return $columnName->columnName->getQuotedName();
},
$item->columnNames
);
if ($item->isPrimary) {
$this->table->setPrimaryKey($columnNames);
$index = $this->table->getPrimaryKey();
} else {
$index = GeneralUtility::makeInstance(
Index::class,
$indexName,
$columnNames,
$item->isUnique,
$item->isPrimary
);
if ($item->isFulltext) {
$index->addFlag('fulltext');
} elseif ($item->isSpatial) {
$index->addFlag('spatial');
}
// Doctrine keys the indexes by a normalized name of its own. Do not rely on that key
// here, but build the list explicitly from the index names, so that re-defining an
// index replaces the previously added one - independent of how Doctrine keys them.
$indexes = [];
foreach ($this->table->getIndexes() as $existingIndex) {
$indexes[strtolower($existingIndex->getName())] = $existingIndex;
}
$indexes[strtolower($index->getName())] = $index;
$this->table = new Table(
$this->table->getQuotedName($this->platform),
$this->table->getColumns(),
array_values($indexes),
[],
$this->table->getForeignKeys(),
$this->table->getOptions()
);
}
return $index;
}
/**
* Prepare an explicit foreign key definition item to be added to the table being built.
*/
protected function addForeignKey(CreateForeignKeyDefinitionItem $item)
{
$indexName = $item->indexName->getQuotedName() ?: null;
$localColumnNames = array_map(
static function (IndexColumnName $columnName): string {
return $columnName->columnName->getQuotedName();
},
$item->columnNames
);
$this->addForeignKeyConstraint($localColumnNames, $item->reference, $indexName);
}
/**
* Add a foreign key constraint to the table being built.
*
* @param string[] $localColumnNames
*/
protected function addForeignKeyConstraint(
array $localColumnNames,
ReferenceDefinition $referenceDefinition,
?string $indexName = null
) {
$foreignTableName = $referenceDefinition->tableName->getQuotedName();
$foreignColumnNames = array_map(
static function (IndexColumnName $columnName): string {
return $columnName->columnName->getQuotedName();
},
$referenceDefinition->columnNames
);
$options = [
'onDelete' => $referenceDefinition->onDelete,
'onUpdate' => $referenceDefinition->onUpdate,
];
$this->table->addForeignKeyConstraint(
$foreignTableName,
$localColumnNames,
$foreignColumnNames,
$options,
$indexName
);
}
/**
* @throws \RuntimeException
*/
protected function getDoctrineColumnTypeName(AbstractDataType $dataType): string
{
switch (get_class($dataType)) {
case TinyIntDataType::class:
// TINYINT is MySQL specific and mapped to a standard SMALLINT
case SmallIntDataType::class:
$doctrineType = Types::SMALLINT;
break;
case MediumIntDataType::class:
// MEDIUMINT is MySQL specific and mapped to a standard INT
case IntegerDataType::class:
$doctrineType = Types::INTEGER;
break;
case BigIntDataType::class:
$doctrineType = Types::BIGINT;
break;
case BinaryDataType::class:
case VarBinaryDataType::class:
// CHAR/VARCHAR is determined by "fixed" column property
$doctrineType = Types::BINARY;
break;
case TinyBlobDataType::class:
case MediumBlobDataType::class:
case BlobDataType::class:
case LongBlobDataType::class:
// Actual field type is determined by field length
$doctrineType = Types::BLOB;
break;
case DateDataType::class:
$doctrineType = Types::DATE_MUTABLE;
break;
case TimestampDataType::class:
case DateTimeDataType::class:
// TIMESTAMP or DATETIME are determined by "version" column property
$doctrineType = Types::DATETIME_MUTABLE;
break;
case NumericDataType::class:
case DecimalDataType::class:
$doctrineType = Types::DECIMAL;
break;
case RealDataType::class:
case FloatDataType::class:
case DoubleDataType::class:
$doctrineType = Types::FLOAT;
break;
case TimeDataType::class:
$doctrineType = Types::TIME_MUTABLE;
break;
case TinyTextDataType::class:
case MediumTextDataType::class:
case TextDataType::class:
case LongTextDataType::class:
$doctrineType = Types::TEXT;
break;
case CharDataType::class:
case VarCharDataType::class:
$doctrineType = Types::STRING;
break;
case EnumDataType::class:
$doctrineType = Types::ENUM;
break;
case SetDataType::class:
$doctrineType = SetType::TYPE;
break;
case JsonDataType::class:
$doctrineType = Types::JSON;
break;
case YearDataType::class:
// The YEAR data type is MySQL specific and offers little to no benefit.
// The two-digit year logic implemented in this data type (1-69 mapped to
// 2001-2069, 70-99 mapped to 1970-1999) can be easily implemented in the
// application and for all other accounts it's an integer with a valid
// range of 1901 to 2155.
// Using a SMALLINT covers the value range and ensures database compatibility.
$doctrineType = Types::SMALLINT;
break;
case UuidDataType::class:
// UUID/GUID is only supported by PostgreSQL for now, but Doctrine DBAL implemented a fallback
// for other platforms and we can safely use `Types::GUID` here in case `UUID` has been set in
// `ext_tables.sql` for a table column.
$doctrineType = Types::GUID;
break;
default:
throw new \RuntimeException(
'Unsupported data type: ' . get_class($dataType) . '!',
1472046376
);
}
return $doctrineType;
}
/**
* Build the table specific options as far as they are supported by Doctrine.
*/
protected function buildTableOptions(array $tableOptions): array
{
$options = [];
if (!empty($tableOptions['engine'])) {
$options['engine'] = (string)$tableOptions['engine'];
}
if (!empty($tableOptions['character_set'])) {
$options['charset'] = (string)$tableOptions['character_set'];
}
if (!empty($tableOptions['collation'])) {
$options['collate'] = (string)$tableOptions['collation'];
}
if (!empty($tableOptions['auto_increment'])) {
$options['auto_increment'] = (string)$tableOptions['auto_increment'];
}
if (!empty($tableOptions['comment'])) {
$options['comment'] = (string)$tableOptions['comment'];
}
if (!empty($tableOptions['row_format'])) {
$options['row_format'] = (string)$tableOptions['row_format'];
}
return $options;
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Doctrine\DBAL\Schema\SchemaDiff as DoctrineSchemaDiff;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff as DoctrineTableDiff;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Based on the doctrine/dbal implementation restoring direct property access
* and adding further helper methods.
*
* @internal not part of public Core API.
*/
class SchemaDiff extends DoctrineSchemaDiff
{
/**
* Constructs an SchemaDiff object.
*
* @internal The diff can be only instantiated by a {@see Comparator}.
*
* @param array<string> $createdSchemas
* @param array<string> $droppedSchemas
* @param array<string, Table> $createdTables
* @param array<string, TableDiff> $alteredTables
* @param array<string, Table> $droppedTables
* @param array<Sequence> $createdSequences
* @param array<Sequence> $alteredSequences
* @param array<Sequence> $droppedSequences
*/
public function __construct(
public array $createdSchemas,
public array $droppedSchemas,
public array $createdTables,
public array $alteredTables,
public array $droppedTables,
public array $createdSequences,
public array $alteredSequences,
public array $droppedSequences,
) {
$this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool {
return !$diff->isEmpty();
});
// NOTE: parent::__construct() not called by intention.
}
/** @return array<string> */
public function getCreatedSchemas(): array
{
return $this->createdSchemas;
}
/** @return array<string> */
public function getDroppedSchemas(): array
{
return $this->droppedSchemas;
}
/** @return array<string, Table> */
public function getCreatedTables(): array
{
return $this->createdTables;
}
/** @return array<string, TableDiff> */
public function getAlteredTables(): array
{
return $this->alteredTables;
}
/** @return array<string, Table> */
public function getDroppedTables(): array
{
return $this->droppedTables;
}
/** @return array<Sequence> */
public function getCreatedSequences(): array
{
return $this->createdSequences;
}
/** @return array<Sequence> */
public function getAlteredSequences(): array
{
return $this->alteredSequences;
}
/** @return array<Sequence> */
public function getDroppedSequences(): array
{
return $this->droppedSequences;
}
/**
* Returns whether the diff is empty (contains no changes).
*/
public function isEmpty(): bool
{
return count($this->createdSchemas) === 0
&& count($this->droppedSchemas) === 0
&& count($this->createdTables) === 0
&& count($this->alteredTables) === 0
&& count($this->droppedTables) === 0
&& count($this->createdSequences) === 0
&& count($this->alteredSequences) === 0
&& count($this->droppedSequences) === 0;
}
public static function ensure(SchemaDiff|DoctrineSchemaDiff $schemaDiff, array $additionalArguments = []): self
{
return new self(...[
'createdSchemas' => $schemaDiff->getCreatedSchemas(),
'droppedSchemas' => $schemaDiff->getDroppedSchemas(),
'createdTables' => self::ensureCollection(...$schemaDiff->getCreatedTables()),
'alteredTables' => self::ensureCollection(...$schemaDiff->getAlteredTables()),
'droppedTables' => self::ensureCollection(...$schemaDiff->getDroppedTables()),
'createdSequences' => $schemaDiff->getCreatedSequences(),
'alteredSequences' => $schemaDiff->getAlteredSequences(),
'droppedSequences' => $schemaDiff->getDroppedSequences(),
...$additionalArguments,
]);
}
/**
* @param DoctrineTableDiff|TableDiff|Table ...$tableDiffs
* @return TableDiff[]|Table[]
*/
public static function ensureCollection(DoctrineTableDiff|TableDiff|Table ...$tableDiffs): array
{
$collection = [];
foreach ($tableDiffs as $key => $tableDiff) {
if ($tableDiff instanceof DoctrineTableDiff) {
$tableDiff = TableDiff::ensure($tableDiff);
}
if (is_int($key) || MathUtility::canBeInterpretedAsInteger($key)) {
$key = $tableDiff instanceof Table
? $tableDiff->getName()
: $tableDiff->getOldTable()->getName();
}
$collection[$key] = $tableDiff;
}
return $collection;
}
}
@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Doctrine\DBAL\Connection;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Database\Schema\Information\ColumnInfo;
use TYPO3\CMS\Core\Database\Schema\Information\TableInfo;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
/**
* This wrapper of SchemaManager contains some internal caches to avoid performance issues for recurring calls to
* specific schema related information. This should only be used in context where no changes are expected to happen.
*
* @internal This class is only for internal core usage and is not part of the public core API.
*/
final class SchemaInformation
{
private string $connectionIdentifier;
public function __construct(
private readonly Connection $connection,
private readonly FrontendInterface $runtime,
private readonly FrontendInterface $cache,
private readonly PackageDependentCacheIdentifier $packageDependentCacheIdentifier,
) {
$this->connectionIdentifier = $this->packageDependentCacheIdentifier
->withPrefix(str_replace(
['.', ':', '/', '\\', '!', '?'],
'_',
(string)($connection->getParams()['dbname'] ?? 'generic')
))
// hash connection params, which holds various information like host,
// port etc. to get a descriptive hash for this connection.
->withAdditionalHashedIdentifier(serialize($connection->getParams()))
->toString();
}
/**
* Similar to doctrine DBAL/AbstractSchemaManager, but with a cache-layer.
* This is used core internally to auto-add types, for instance in Connection::insert().
*
* @return string[]
*/
public function listTableNames(): array
{
$identifier = $this->connectionIdentifier . '-tablenames';
// Level 1 cache
$tableNames = $this->runtime->get($identifier);
if (is_array($tableNames)) {
return $tableNames;
}
// Level 2 cache
$tableNames = $this->cache->get($identifier);
if (is_array($tableNames)) {
// Retrieved from level 2, set to level 1 cache.
$this->runtime->set($identifier, $tableNames);
return $tableNames;
}
return $this->buildTableNames();
}
/**
* @param string $tableName
* @return array<string, ColumnInfo>
*/
public function listTableColumnInfos(string $tableName): array
{
return $this->getTableInfo($tableName)->getColumnInfos();
}
/**
* @param string $tableName
* @return string[]
*/
public function listTableColumnNames(string $tableName): array
{
return $this->getTableInfo($tableName)->getColumnNames();
}
public function getTableInfo(string $tableName): TableInfo
{
$identifier = $this->connectionIdentifier . '-tableinfo-' . $tableName;
$tableInfo = $this->runtime->get($identifier);
// Level 1 cache
if ($tableInfo instanceof TableInfo) {
return $tableInfo;
}
// Level 2 cache
$tableInfo = $this->cache->get($identifier);
if ($tableInfo instanceof TableInfo) {
// Retrieved from level 2, set to level 1 cache.
$this->runtime->set($identifier, $tableInfo);
return $tableInfo;
}
return $this->buildTableInformation($tableName);
}
/**
* @return string[]
*/
private function buildTableNames(): array
{
$identifier = $this->connectionIdentifier . '-tablenames';
$names = array_values($this->connection->createSchemaManager()->listTableNames());
// Level 1 cache
$this->runtime->set($identifier, $names);
// Level 2 cache
$this->cache->set($identifier, $names);
return $names;
}
private function buildTableInformation(string $tableName): TableInfo
{
$identifier = $this->connectionIdentifier . '-tableinfo-' . $tableName;
// Transform doctrine columns into ColumnInfo and add to new associative array using column name with
// unmodified casing as array keys and not the lowercased from doctrine dbal associative array, which
// leads to comparison issues in the core using the names. We need the untouched casing.
$columns = $this->connection->createSchemaManager()->listTableColumns($tableName);
$columnInfos = [];
foreach ($columns as $column) {
$columnInfo = ColumnInfo::convertFromDoctrineColumn($column);
$columnInfos[$columnInfo->name] = $columnInfo;
}
$tableInfo = new TableInfo(
name: $tableName,
columnInfos: $columnInfos,
);
// Level 1 cache
$this->runtime->set($identifier, $tableInfo);
// Level 2 cache
$this->cache->set($identifier, $tableInfo);
return $tableInfo;
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\SchemaManager;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
/**
* Provides a couple of methods use-full to restore Doctrine DBAL 3 and earlier behaviour to
* respect doctrine type override within column comment.
*
* Used within:
*
* - {@see MySQLSchemaManager::parentGetPortableTableColumnDefinition()}
* - {@see PostgreSQLSchemaManager::parentGetPortableTableColumnDefinition()}
* - {@see SQLiteSchemaManager::parentGetPortableTableColumnDefinition()}
*
* This trait contains code cloned or adopted from Doctrine DBAL 3 removed from {@see AbstractSchemaManager} hierarchy:
*
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/AbstractSchemaManager.php#L1730-L1752
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/AbstractSchemaManager.php#L1730-L1752
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/SqliteSchemaManager.php#L338-L344
*
* @internal for use in extended {@see AbstractSchemaManager} hierarchy and not part of public Core API.
*/
trait ColumnTypeCommentMethodsTrait
{
/**
* Determine Doctrine Type based on column database type with respecting
* comment definition as overrule type. That reflects the old behaviour
* of Doctrine DBAL 3 and older.
*
* Code has been adopted from Doctrine DBAL v3 to be used and integrated within extended {@see AbstractSchemaManager}
* hierarchy to restore removed behaviour, see:
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/MySQLSchemaManager.php#L186-L192
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/PostgreSQLSchemaManager.php#L427-L429
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/SqliteSchemaManager.php#L338-L344
*/
private function determineColumnType(string $dbType, array &$tableColumn): string
{
$platform = ($this instanceof AbstractPlatform) ? $this : $this->platform;
$type = $dbType !== '' ? $platform->getDoctrineTypeMapping($dbType) : '';
if (isset($tableColumn['comment'])) {
$type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
$tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
}
return $type;
}
/**
* Doctrine DBAL 4 removed this from the {@see AbstractSchemaManager} hierarchy, and is here cloned, see:
* https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/AbstractSchemaManager.php#L1730-L1752
*
* Given a table comment this method tries to extract a typehint for Doctrine Type, or returns
* the type given as default.
*
* @param string|null $comment
* @param string $currentType
*
* @return string
*@internal This method should be only used from within the extended AbstractSchemaManager class hierarchy.
*/
private function extractDoctrineTypeFromComment(?string $comment, string $currentType): string
{
if ($comment !== null && preg_match('(\(DC2Type:(((?!\)).)+)\))', $comment, $match) === 1) {
return $match[1];
}
return $currentType;
}
/**
* Doctrine DBAL 4 removed this from the {@see AbstractSchemaManager} hierarchy, and is here cloned, see:
* https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/AbstractSchemaManager.php#L1754-L1773
*
* @param string|null $comment
* @param string|null $type
*
* @return string|null
*@internal This method should be only used from within the extended AbstractSchemaManager class hierarchy.
*/
private function removeDoctrineTypeFromComment(?string $comment = null, ?string $type = null): ?string
{
if ($comment === null) {
return null;
}
return str_replace('(DC2Type:' . $type . ')', '', $comment);
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\SchemaManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform as DoctrineAbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform as DoctrinePostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SQLitePlatform as DoctrineSQLitePlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\SchemaManagerFactory;
/**
* Custom SchemaManager factory to ensure that the extended SchemaManager
* classes are used for supported platforms. Without this, custom schema
* handling would be cut off.
*
* Note: This is the transition to mitigate the dropped doctrine event manager
* since `doctrine/dbal ^4`.
*
* @see https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-not-setting-a-schema-manager-factory
* @see https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-extension-via-schema-definition-events
*
* @internal for core internal usage and not part of public core API.
*/
final class CoreSchemaManagerFactory implements SchemaManagerFactory
{
public function createSchemaManager(Connection $connection): AbstractSchemaManager
{
$platform = $connection->getDatabasePlatform();
// Platform specific SchemaManager are extended to manipulate the schema handling. TYPO3 needs to
// do that to provide additional doctrine type handling and other workarounds or alignments. Long
// time this have been done by using the `doctrine EventManager` to hook into several places, which
// no longer exists.
//
// @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-not-setting-a-schema-manager-factory
// @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-extension-via-doctrine-event-manager
// @todo Consider make check on SchemaManager instance retrieved from $platform->createSchemaManager()
return match (true) {
$platform instanceof DoctrineSQLitePlatform => new SQLiteSchemaManager($connection, $platform),
$platform instanceof DoctrinePostgreSQLPlatform => new PostgreSQLSchemaManager($connection, $platform),
$platform instanceof DoctrineMariaDBPlatform,
$platform instanceof DoctrineMySQLPlatform,
$platform instanceof DoctrineAbstractMySQLPlatform => new MySQLSchemaManager($connection, $platform),
default => $platform->createSchemaManager($connection),
};
}
}
@@ -0,0 +1,392 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\SchemaManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\MySQLSchemaManager as DoctrineMySQLSchemaManager;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
/**
* Extending the doctrine MySQLSchemaManager to integrate additional processing stuff
* due to the dropped event system with `doctrine/dbal 4.x`.
*
* For example, this is used to process custom doctrine types.
*
* Platform specific SchemaManager are extended to manipulate the schema handling. TYPO3 needs to
* do that to provide additional doctrine type handling and other workarounds or alignments. Long
* time this have been done by using the `doctrine EventManager` to hook into several places, which
* no longer exists.
*
* Note: MySQLSchemaManager is used for MySQL and MariaDB. Even doctrine/dbal 4.0 provides no dedicated
* schema manager for doctrine/dbal 4.0. Keep this in mind.
*
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-not-setting-a-schema-manager-factory
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-extension-via-doctrine-event-manager
*
* @internal not part of the public Core API.
*/
class MySQLSchemaManager extends DoctrineMySQLSchemaManager
{
use ColumnTypeCommentMethodsTrait;
/** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */
private const MARIADB_ESCAPE_SEQUENCES = [
'\\0' => "\0",
"\\'" => "'",
'\\"' => '"',
'\\b' => "\b",
'\\n' => "\n",
'\\r' => "\r",
'\\t' => "\t",
'\\Z' => "\x1a",
'\\\\' => '\\',
'\\%' => '%',
'\\_' => '_',
// Internally, MariaDB escapes single quotes using the standard syntax
"''" => "'",
];
private const array MYSQL_ESCAPE_SEQUENCES = [
'\\0' => "\0",
"\\'" => "'",
'\\"' => '"',
'\\b' => "\b",
'\\n' => "\n",
'\\r' => "\r",
'\\t' => "\t",
'\\Z' => "\x1a",
'\\\\' => '\\',
'\\%' => '%',
'\\_' => '_',
// internally
"''" => "'",
];
private const array MYSQL_UNQUOTE_SEQUENCES = [
"\\'" => "'",
'\\"' => '"',
];
/**
* Gets Table Column Definition.
*
* @param array<string, mixed> $tableColumn
*/
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
{
/** @var DoctrineMariaDBPlatform|DoctrineMySQLPlatform $platform */
$platform = $this->platform;
$tableColumn = $this->normalizeTableColumnData($tableColumn, $platform);
return $this->parentGetPortableTableColumnDefinition($tableColumn);
}
/**
* @param array<string, mixed> $tableColumn
* @return array<string, mixed>
*/
protected function normalizeTableColumnData(array $tableColumn, DoctrineMariaDBPlatform|DoctrineMySQLPlatform $platform): array
{
if (!($platform instanceof DoctrineMySQLPlatform)) {
return $tableColumn;
}
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$dbType = strtolower($tableColumn['type']);
$columnDefault = $tableColumn['default'] ?? null;
$type = Type::getType($platform->getDoctrineTypeMapping($dbType));
if ($type instanceof TextType || $type instanceof BlobType || $type instanceof JsonType) {
$tableColumn['default'] = $this->getMySQLTextAndBlobColumnDefault($columnDefault);
}
return $tableColumn;
}
protected function getMySQLTextAndBlobColumnDefault(?string $columnDefault): ?string
{
if ($columnDefault === null || $columnDefault === 'NULL') {
return null;
}
if (str_starts_with($columnDefault, '_')) {
$columnDefault = substr($columnDefault, (mb_strpos($columnDefault, '\'') - 1));
}
if ($columnDefault === "\'\'") {
return '';
}
if (preg_match("/^\\\'(.*)\\\'$/", trim($columnDefault), $matches) === 1) {
return strtr(
strtr($matches[1], self::MYSQL_ESCAPE_SEQUENCES),
// MySQL saves quoted single-quote as escaped single-quote in the INFORMATION SCHEMA table, even
// if it has been provided with double-quote quoting and is inconsistent for itself and enforces
// a additional unquoting after the un-escaping step
self::MYSQL_UNQUOTE_SEQUENCES
);
}
return $columnDefault;
}
/**
* @param array<int, array<string, mixed>> $tableIndexes
* @param string $tableName
*
* @return array<string, Index>
*/
protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
{
// Get doctrine generated list.
$tableIndexesList = parent::_getPortableTableIndexesList(
// tableIndexes
$tableIndexes,
// tableName
$tableName,
);
// Concatenate index prefix length to column name
// @todo Adapt TYPO3 schema comparison to use Index::getOption('lengths')
// instead of assuming that the length is concatenated to the column name.
return array_map(
static function (Index $index): Index {
if (!$index->hasOption('lengths')) {
return $index;
}
$options = $index->getOptions();
$lengths = $options['lengths'];
unset($options['lengths']);
$columns = $index->getColumns();
foreach ($columns as $id => $column) {
if (!isset($lengths[$id])) {
continue;
}
$columns[$id] = $column . '(' . $lengths[$id] . ')';
}
return new Index(
$index->getName(),
$columns,
$index->isUnique(),
$index->isPrimary(),
$index->getFlags(),
$options
);
},
$tableIndexesList
);
}
/**
* Gets Table Column Definition.
*
* This is a copy of {@see DoctrineMySQLSchemaManager::_getPortableTableColumnDefinition()} with a minor change
* to respect column comments for Doctrine Type matching and thus restoring Doctrine DBAL behaviour before v4.x.
*
* @param array $tableColumn
*
* @throws Exception
*/
private function parentGetPortableTableColumnDefinition(array $tableColumn): Column
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$dbType = $tableColumn['type'];
$length = null;
$scale = 0;
$precision = null;
$fixed = false;
$values = [];
// This is the change required for TYPO3 - rest of method is kept (cloned) from original.
// Following line differs from \Doctrine\DBAL\Schema\MySQLSchemaManager::_getPortableTableColumnDefinition,
// taken from:
// - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/MySQLSchemaManager.php#L186-L192
$type = $this->determineColumnType($dbType, $tableColumn);
switch ($dbType) {
case 'char':
case 'varchar':
$length = $tableColumn['character_maximum_length'];
break;
case 'binary':
case 'varbinary':
$length = $tableColumn['character_octet_length'];
break;
case 'tinytext':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
break;
case 'text':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
break;
case 'mediumtext':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
break;
case 'tinyblob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
break;
case 'blob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
break;
case 'mediumblob':
$length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
break;
case 'float':
case 'double':
case 'real':
case 'numeric':
case 'decimal':
$precision = $tableColumn['numeric_precision'];
if (isset($tableColumn['numeric_scale'])) {
$scale = $tableColumn['numeric_scale'];
}
break;
}
switch ($dbType) {
case 'char':
case 'binary':
$fixed = true;
break;
case 'enum':
$values = $this->parseEnumExpression($tableColumn['column_type']);
break;
case 'set':
// --------------------------------------------------------------
// `SET` handling and parsing is a custom TYPO3 implementation
// --------------------------------------------------------------
$values = $this->parseSetExpression($tableColumn['column_type']);
// --------------------------------------------------------------
}
if ($this->platform instanceof MariaDBPlatform) {
$columnDefault = $this->getMariaDBColumnDefault($this->platform, $tableColumn['default']);
} else {
$columnDefault = $tableColumn['default'];
}
$options = [
'length' => $length,
'unsigned' => str_contains($tableColumn['column_type'], 'unsigned'),
'fixed' => $fixed,
'default' => $columnDefault,
'notnull' => $tableColumn['null'] !== 'YES',
'scale' => $scale,
'precision' => $precision,
'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'),
'values' => $values,
];
if (isset($tableColumn['comment'])) {
$options['comment'] = $tableColumn['comment'];
}
$column = new Column($tableColumn['field'], Type::getType($type), $options);
$column->setPlatformOption('charset', $tableColumn['characterset']);
$column->setPlatformOption('collation', $tableColumn['collation']);
return $column;
}
/**
* Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
*
* - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
* to distinguish them from expressions (see MDEV-10134).
* - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
* as current_timestamp(), currdate(), currtime()
* - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
* null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
* - \' is always stored as '' in information_schema (normalized)
*
* @link https://mariadb.com/kb/en/library/information-schema-columns-table/
* @link https://jira.mariadb.org/browse/MDEV-13132
*
* Copy of {@see DoctrineMySQLSchemaManager::getMariaDBColumnDefault()}
*
* @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
*/
private function getMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string
{
if ($columnDefault === 'NULL' || $columnDefault === null) {
return null;
}
if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) {
return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
}
return match ($columnDefault) {
'current_timestamp()' => $platform->getCurrentTimestampSQL(),
'curdate()' => $platform->getCurrentDateSQL(),
'curtime()' => $platform->getCurrentTimeSQL(),
default => $columnDefault,
};
}
/**
* Cloned from {@see DoctrineMySQLSchemaManager::parseEnumExpression()} (4.3.x).
*
* @return list<string>
*/
private function parseEnumExpression(string $expression): array
{
$result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches);
assert($result !== false);
return array_map(
static fn(string $match): string => strtr($match, ["''" => "'"]),
$matches[1],
);
}
/**
* Adopted from {@see DoctrineMySQLSchemaManager::parseEnumExpression()} (4.3.x).
*
* @return list<string>
*/
private function parseSetExpression(string $expression): array
{
$result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches);
assert($result !== false);
return array_map(
static fn(string $match): string => strtr($match, ["''" => "'"]),
$matches[1],
);
}
}
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\SchemaManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\PostgreSQLSchemaManager as DoctrinePostgreSQLSchemaManager;
use Doctrine\DBAL\Types\JsonType;
use Doctrine\DBAL\Types\Type;
/**
* Extending the doctrine PostgreSQLSchemaManager to integrate additional processing stuff
* due to the dropped event system with `doctrine/dbal 4.x`.
*
* For example, this is used to process custom doctrine types.
*
* Platform specific SchemaManager are extended to manipulate the schema handling. TYPO3 needs to
* do that to provide additional doctrine type handling and other workarounds or alignments. Long
* time this have been done by using the `doctrine EventManager` to hook into several places, which
* no longer exists.
*
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-not-setting-a-schema-manager-factory
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-extension-via-doctrine-event-manager
*
* @internal not part of the public Core API.
*/
class PostgreSQLSchemaManager extends DoctrinePostgreSQLSchemaManager
{
use ColumnTypeCommentMethodsTrait;
/**
* Gets Table Column Definition.
*
* @param array<string, mixed> $tableColumn
*/
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
{
return $this->parentGetPortableTableColumnDefinition($tableColumn);
}
/**
* Gets Table Column Definition.
*
* This is a copy of {@see DoctrinePostgreSQLSchemaManager::_getPortableTableColumnDefinition()} with a minor change
* to respect column comments for Doctrine Type matching and thus restoring Doctrine DBAL behaviour before v4.x.
*
* @param array $tableColumn
*
* @throws Exception
*/
protected function parentGetPortableTableColumnDefinition(array $tableColumn): Column
{
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
$length = null;
$precision = null;
$scale = 0;
$fixed = false;
$jsonb = false;
$dbType = $tableColumn['type'];
if (
$tableColumn['domain_type'] !== null
&& ! $this->platform->hasDoctrineTypeMappingFor($dbType)
) {
$dbType = $tableColumn['domain_type'];
$completeType = $tableColumn['domain_complete_type'];
} else {
$completeType = $tableColumn['complete_type'];
}
// This is the change required for TYPO3 - rest of method is kept (cloned) from original.
// Following line differs from \Doctrine\DBAL\Schema\MySQLSchemaManager::_getPortableTableColumnDefinition,
// taken from:
// - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/PostgreSQLSchemaManager.php#L427-L429
$type = $this->determineColumnType($dbType, $tableColumn);
switch ($dbType) {
case 'bpchar':
case 'varchar':
$parameters = $this->parseColumnTypeParameters($completeType);
if (count($parameters) > 0) {
$length = $parameters[0];
}
break;
case 'double':
case 'decimal':
case 'money':
case 'numeric':
$parameters = $this->parseColumnTypeParameters($completeType);
if (count($parameters) > 0) {
$precision = $parameters[0];
}
if (count($parameters) > 1) {
$scale = $parameters[1];
}
break;
}
if ($dbType === 'bpchar') {
$fixed = true;
} elseif ($dbType === 'jsonb') {
$jsonb = true;
}
$options = [
'length' => $length,
'notnull' => (bool)$tableColumn['isnotnull'],
'default' => $this->parseDefaultExpression($tableColumn['default']),
'precision' => $precision,
'scale' => $scale,
'fixed' => $fixed,
'autoincrement' => $tableColumn['attidentity'] === 'd',
];
if ($tableColumn['comment'] !== null) {
$options['comment'] = $tableColumn['comment'];
}
$column = new Column($tableColumn['field'], Type::getType($type), $options);
if (! empty($tableColumn['collation'])) {
$column->setPlatformOption('collation', $tableColumn['collation']);
}
if ($column->getType() instanceof JsonType) {
$column->setPlatformOption('jsonb', $jsonb);
}
return $column;
}
/**
* Parses a default value expression as given by PostgreSQL
*
* Copy of {@see DoctrinePostgreSQLSchemaManager::parseDefaultExpression()} (Doctrine DBAL 4.3.x)
*/
private function parseDefaultExpression(?string $expression): mixed
{
if ($expression === null || str_starts_with($expression, 'NULL::')) {
return null;
}
if ($expression === 'true') {
return true;
}
if ($expression === 'false') {
return false;
}
if (preg_match("/^'(.*)'::/s", $expression, $matches) === 1) {
return str_replace("''", "'", $matches[1]);
}
return $expression;
}
/**
* Parses the parameters between parenthesis in the data type.
*
* Copy of {@see DoctrinePostgreSQLSchemaManager::parseColumnTypeParameters()}
*
* @return list<int>
*/
private function parseColumnTypeParameters(string $type): array
{
if (preg_match('/\((\d+)(?:,(\d+))?\)/', $type, $matches) !== 1) {
return [];
}
$parameters = [(int)$matches[1]];
if (isset($matches[2])) {
$parameters[] = (int)$matches[2];
}
return $parameters;
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\SchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\SQLiteSchemaManager as DoctrineSQLiteSchemaManager;
use Doctrine\DBAL\Types\Type;
/**
* Extending the doctrine SQLiteSchemaManager to integrate additional processing stuff
* due to the dropped event system with `doctrine/dbal 4.x`.
*
* For example, this is used to process custom doctrine types.
*
* Platform specific SchemaManager are extended to manipulate the schema handling. TYPO3 needs to
* do that to provide additional doctrine type handling and other workarounds or alignments. Long
* time this have been done by using the `doctrine EventManager` to hook into several places, which
* no longer exists.
*
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-not-setting-a-schema-manager-factory
* @link https://github.com/doctrine/dbal/blob/3.7.x/UPGRADE.md#deprecated-extension-via-doctrine-event-manager
*
* @internal not part of the public Core API.
*/
class SQLiteSchemaManager extends DoctrineSQLiteSchemaManager
{
use ColumnTypeCommentMethodsTrait;
/**
* Doctrine DBAL v4 dropped column comment based type api, which TYPO3 still needs. To mitigate this, this
* method is overridden to reapply the type comment removal, adopted from:
*
* - https://github.com/doctrine/dbal/blob/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba/src/Schema/SqliteSchemaManager.php#L338-L344
*
* by using {@see ColumnTypeCommentMethodsTrait::determineColumnType()} to reuse methods.
*/
protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array
{
$list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
foreach ($list as $columnName => $column) {
$fakeTableColumn = [
'type' => $column->getType(),
'comment' => $column->getComment(),
];
$type = $this->determineColumnType('', $fakeTableColumn);
if ($type !== '') {
$column->setType(Type::getType($type));
}
$column->setComment($fakeTableColumn['comment']);
}
return $list;
}
/**
* Gets Table Column Definition.
*
* @param array<string, mixed> $tableColumn
*/
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
{
return parent::_getPortableTableColumnDefinition($tableColumn);
}
}
+515
View File
@@ -0,0 +1,515 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\SchemaDiff;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* Helper methods to handle SQL files and transform them into individual statements
* for further processing.
*
* @internal not part of public core API.
*/
#[Autoconfigure(public: true)]
readonly class SchemaMigrator
{
public function __construct(
private ConnectionPool $connectionPool,
private Parser $parser,
private DefaultTcaSchema $defaultTcaSchema,
private TcaSchemaFactory $tcaSchemaFactory,
#[Autowire(service: 'cache.runtime')]
private FrontendInterface $runtime,
) {}
/**
* Compare current and expected schema definitions and provide updates suggestions in the form
* of SQL statements.
*
* @param string[] $statements The CREATE TABLE statements
* @param bool $remove TRUE for RENAME/DROP table and column suggestions, FALSE for ADD/CHANGE suggestions
* @return array<string, array> SQL statements to migrate the database to the expected schema, indexed by performed operation
* @throws DBALException
* @throws SchemaException
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws StatementException
*/
public function getUpdateSuggestions(array $statements, bool $remove = false): array
{
$tables = $this->parseCreateTableStatements($statements);
$updateSuggestions = [];
foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
$connection = $this->connectionPool->getConnectionByName($connectionName);
$connectionMigrator = new ConnectionMigrator($connectionName, $connection, $this->connectionPool, $tables);
$updateSuggestions[$connectionName] = $connectionMigrator->getUpdateSuggestions($remove);
}
return $updateSuggestions;
}
/**
* Return the raw Doctrine SchemaDiff objects for each connection. This diff contains
* all changes without any pre-processing.
*
* @return array<string, SchemaDiff>
* @throws DBALException
* @throws SchemaException
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws StatementException
*/
public function getSchemaDiffs(array $statements): array
{
$tables = $this->parseCreateTableStatements($statements);
$schemaDiffs = [];
foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
$connection = $this->connectionPool->getConnectionByName($connectionName);
$connectionMigrator = new ConnectionMigrator($connectionName, $connection, $this->connectionPool, $tables);
$schemaDiffs[$connectionName] = $connectionMigrator->getSchemaDiff();
}
return $schemaDiffs;
}
/**
* This method executes statements from the update suggestions, or a subset of them
* filtered by the statements hashes, one by one.
*
* @param string[] $statements The CREATE TABLE statements
* @param string[] $selectedStatements The hashes of the update suggestions to execute
* @throws DBALException
* @throws SchemaException
* @throws \InvalidArgumentException
* @throws StatementException
* @throws \RuntimeException
*/
public function migrate(array $statements, array $selectedStatements): array
{
$result = [];
$updateSuggestionsPerConnection = array_replace_recursive(
$this->getUpdateSuggestions($statements),
$this->getUpdateSuggestions($statements, true)
);
foreach ($updateSuggestionsPerConnection as $connectionName => $updateSuggestions) {
unset($updateSuggestions['tables_count'], $updateSuggestions['change_currentValue']);
$updateSuggestions = array_merge(...array_values($updateSuggestions));
$statementsToExecute = array_intersect_key($updateSuggestions, $selectedStatements);
if (count($statementsToExecute) === 0) {
continue;
}
$connection = $this->connectionPool->getConnectionByName($connectionName);
foreach ($statementsToExecute as $hash => $statement) {
try {
$connection->executeStatement($statement);
} catch (DBALException $e) {
$result[$hash] = $e->getMessage();
}
}
}
$this->flushDatabaseSchemaCache();
return $result;
}
/**
* Perform add/change/create operations on tables and fields in an optimized, non-interactive, mode.
*
* @param string[] $statements The CREATE TABLE statements
* @param bool $createOnly Only perform changes that add fields or create tables
* @return array<string, string> Error messages for statements that occurred during the installation procedure.
* @throws DBALException
* @throws SchemaException
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws StatementException
*/
public function install(array $statements, bool $createOnly = false): array
{
$tables = $this->parseCreateTableStatements($statements);
$result = [];
foreach ($this->connectionPool->getConnectionNames() as $connectionName) {
$connection = $this->connectionPool->getConnectionByName($connectionName);
$connectionMigrator = new ConnectionMigrator($connectionName, $connection, $this->connectionPool, $tables);
$lastResult = $connectionMigrator->install($createOnly);
$result = array_merge($result, $lastResult);
}
$this->flushDatabaseSchemaCache();
return $result;
}
/**
* Import static data (INSERT statements)
*/
public function importStaticData(array $statements, bool $truncate = false): array
{
$result = [];
$insertStatements = [];
foreach ($statements as $statement) {
// Only handle insert statements and extract the table at the same time. Extracting
// the table name is required to perform the inserts on the right connection.
if (preg_match('/^INSERT\s+INTO\s+`?(\w+)`?(.*)/i', $statement, $matches)) {
[, $tableName, $sqlFragment] = $matches;
$insertStatements[$tableName][] = sprintf(
'INSERT INTO %s %s',
$this->connectionPool->getConnectionForTable($tableName)->quoteIdentifier($tableName),
rtrim($sqlFragment, ';')
);
}
}
foreach ($insertStatements as $tableName => $perTableStatements) {
$connection = $this->connectionPool->getConnectionForTable($tableName);
if ($truncate) {
$connection->truncate($tableName);
}
foreach ((array)$perTableStatements as $statement) {
try {
$connection->executeStatement($statement);
$result[$statement] = '';
} catch (DBALException $e) {
$result[$statement] = $e->getMessage();
}
}
}
return $result;
}
/**
* Parse CREATE TABLE statements into Doctrine Table objects.
*
* @param string[] $statements The SQL CREATE TABLE statements
* @return array<non-empty-string, Table>
* @throws SchemaException
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws StatementException
*/
protected function parseCreateTableStatements(array $statements): array
{
$tables = $this->prepareTablesFromStatements($statements);
$tables = $this->ensureTableDefinitionForAllTCAManagedTables($tables);
$tables = $this->mergeTableDefinitions($tables);
$tables = $this->enrichTablesFromDefaultTCASchema($tables);
$tables = $this->ensureDefaultTCAFieldsAreOrdered($tables);
return $tables;
}
/**
* Have fields triggered by 'ctrl' settings first in the list. This is done for cosmetic
* reasons to improve readability of db schema when opening tables in a database browser.
*
* @return string[]
*/
protected function getPrioritizedFieldNames(string $tableName): array
{
if (!$this->tcaSchemaFactory->has($tableName)) {
return [];
}
$prioritizedFieldNames = [
'uid',
'pid',
];
$tableSchema = $this->tcaSchemaFactory->get($tableName);
if ($tableSchema->hasCapability(TcaSchemaCapability::CreatedAt)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::CreatedAt)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::UpdatedAt)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::UpdatedAt)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::SoftDelete)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName();
}
if ($tableSchema->isLanguageAware()) {
$languageField = $tableSchema->getCapability(TcaSchemaCapability::Language);
$prioritizedFieldNames[] = $languageField->getLanguageField()->getName();
$prioritizedFieldNames[] = $languageField->getTranslationOriginPointerField()->getName();
// @todo `l10n_state` is automatically added in `DefaultTcaSchema->enrichSingleTableFieldsFromTcaCtrl()`
// if `ctrl->languageField` and `ctrl->transOrigPointerField` are configured, and not provided
// by extension `ext_tables.sql`. This field has no representation in TcaSchema language field
// handling yet, nor is this covered within TcaEnrichment thus adding it here directly for now.
$prioritizedFieldNames[] = 'l10n_state';
if (!empty($languageField->hasTranslationSourceField())) {
$prioritizedFieldNames[] = $languageField->getTranslationSourceField()->getName();
}
if (!empty($languageField->hasDiffSourceField())) {
$prioritizedFieldNames[] = $languageField->getDiffSourceField()->getName();
}
}
if ($tableSchema->hasCapability(TcaSchemaCapability::SortByField)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::SortByField)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::InternalDescription)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::InternalDescription)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::EditLock)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName();
}
if ($tableSchema->hasCapability(TcaSchemaCapability::AncestorReferenceField)) {
$prioritizedFieldNames[] = $tableSchema->getCapability(TcaSchemaCapability::AncestorReferenceField)->getFieldName();
}
if ($tableSchema->isWorkspaceAware()) {
// @todo Adding hardcoded field names directly thus not having a representation within the TcaSchema. These
// fields do not get proper TCA either within `TcaEnrichment` albeit ensured to be created within
// `DefaultTcaSchema->enrichSingleTableFieldsFromTcaCtrl()` as soon as `ctr->versioningWS` is true.
$prioritizedFieldNames[] = 't3ver_wsid';
$prioritizedFieldNames[] = 't3ver_oid';
$prioritizedFieldNames[] = 't3ver_state';
$prioritizedFieldNames[] = 't3ver_stage';
}
return $prioritizedFieldNames;
}
/**
* To give extensions the ability to extend or modify the database schema for core or other extension tables, a
* collection of DDL statement parts are parsed into partial table classes. This method merges the table definition
* parts to end up with a single table representation to ease further handling.
*
* @param Table[] $tables
* @return array<non-empty-string, Table>
*/
private function mergeTableDefinitions(array $tables): array
{
$return = [];
foreach ($tables as $table) {
$tableName = $this->trimIdentifierQuotes($table->getName());
if (!array_key_exists($tableName, $return)) {
$return[$tableName] = $table;
continue;
}
// Merge multiple table definitions. Later definitions overrule identical
// columns, indexes and foreign_keys. Order of definitions is based on
// extension load order.
$currentTableDefinition = $return[$tableName];
$return[$tableName] = new Table(
$tableName,
$this->mergeColumns(...$currentTableDefinition->getColumns(), ...$table->getColumns()),
$this->mergeIndexes(...array_values($currentTableDefinition->getIndexes()), ...array_values($table->getIndexes())),
[],
$this->mergeForeignKeys(...array_values($currentTableDefinition->getForeignKeys()), ...array_values($table->getForeignKeys())),
array_merge($currentTableDefinition->getOptions(), $table->getOptions())
);
}
return $return;
}
/**
* @param Column ...$columns
* @return Column[]
*/
private function mergeColumns(Column ...$columns): array
{
$mergedColumns = [];
foreach ($columns as $column) {
$mergedColumns[$column->getName()] = $column;
}
return array_values($mergedColumns);
}
/**
* @param Index ...$indexes
* @return Index[]
*/
private function mergeIndexes(Index ...$indexes): array
{
$mergedIndexes = [];
foreach ($indexes as $index) {
$mergedIndexes[$index->getName()] = $index;
}
return array_values($mergedIndexes);
}
/**
* Unnamed foreign key constraints cannot be identified by name and are therefore kept as they are.
* Doctrine generates a name for them, but only as the array key - not on the constraint itself.
*
* @param ForeignKeyConstraint ...$foreignKeys
* @return ForeignKeyConstraint[]
*/
private function mergeForeignKeys(ForeignKeyConstraint ...$foreignKeys): array
{
$mergedForeignKeys = [];
foreach ($foreignKeys as $foreignKey) {
$foreignKeyName = $foreignKey->getName();
if ($foreignKeyName === '') {
$mergedForeignKeys[] = $foreignKey;
continue;
}
$mergedForeignKeys[$foreignKeyName] = $foreignKey;
}
return array_values($mergedForeignKeys);
}
/**
* Trim all possible identifier quotes from identifier. This method has been cloned from Doctrine DBAL.
*
* @see \Doctrine\DBAL\Schema\AbstractAsset::trimQuotes()
*/
private function trimIdentifierQuotes(string $identifier): string
{
return str_replace(['`', '"', '[', ']'], '', $identifier);
}
/**
* @param string[] $statements
* @return Table[]
* @throws SchemaException
* @throws StatementException
*/
protected function prepareTablesFromStatements(array $statements): array
{
$tables = [];
foreach ($statements as $statement) {
// We need to keep multiple table definitions at this point so
// that Extensions can modify existing tables.
try {
$tables[] = $this->parser->parse($statement);
} catch (StatementException $statementException) {
// Enrich the error message with the full invalid statement
throw new StatementException(
$statementException->getMessage() . ' in statement: ' . LF . $statement,
1476171315,
$statementException
);
}
}
// Flatten the array of arrays by one level
$tables = array_merge(...$tables);
return $tables;
}
/**
* Ensure we have a table definition for all tables within TCA, add missing ones
* as "empty" tables without columns. This is needed for DefaultTcaSchema: It goes
* through TCA to add columns automatically, but needs a table definition of all
* TCA tables. We're not doing this in DefaultTcaSchema to not introduce a dependency
* to the Parser class in there, which we have here so conveniently already.
*
* @param Table[] $tables
* @return Table[]
* @throws SchemaException
* @throws StatementException
*/
protected function ensureTableDefinitionForAllTCAManagedTables(array $tables): array
{
$tableNamesFromTca = $this->tcaSchemaFactory->all()->getNames();
$tableNamesFromExtTables = [];
foreach ($tables as $table) {
$tableNamesFromExtTables[] = $table->getName();
}
$tableNamesFromExtTables = array_unique($tableNamesFromExtTables);
$missingTableNames = array_diff($tableNamesFromTca, $tableNamesFromExtTables);
foreach ($missingTableNames as $tableName) {
$createTableSql = 'CREATE TABLE ' . $tableName . '();';
$tables[] = $this->parser->parse($createTableSql)[0];
}
return $tables;
}
/**
* @param array<non-empty-string, Table> $tables
* @return array<non-empty-string, Table>
*/
protected function enrichTablesFromDefaultTCASchema(array $tables): array
{
return $this->defaultTcaSchema->enrich($tables);
}
/**
* Ensure the default TCA fields are ordered.
*
* @param array<non-empty-string, Table> $tables
* @return array<non-empty-string, Table>
*/
protected function ensureDefaultTCAFieldsAreOrdered(array $tables): array
{
foreach ($tables as $k => $table) {
$prioritizedColumnNames = $this->getPrioritizedFieldNames($table->getName());
// no TCA table
if (empty($prioritizedColumnNames)) {
continue;
}
$prioritizedColumns = [];
$nonPrioritizedColumns = [];
foreach ($table->getColumns() as $columnObject) {
if (in_array($columnObject->getName(), $prioritizedColumnNames, true)) {
$prioritizedColumns[] = $columnObject;
} else {
$nonPrioritizedColumns[] = $columnObject;
}
}
$tables[$k] = new Table(
$table->getName(),
array_merge($prioritizedColumns, $nonPrioritizedColumns),
$table->getIndexes(),
[],
$table->getForeignKeys(),
$table->getOptions()
);
}
return $tables;
}
protected function flushDatabaseSchemaCache(): void
{
Bootstrap::createCache('database_schema')->flush();
$this->runtime->flush();
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent;
use TYPO3\CMS\Core\Package\PackageManager;
/**
* Helper methods to handle raw SQL input and transform it into individual statements
* for further processing.
*
* @internal not part of public core API.
*/
#[Autoconfigure(public: true)]
class SqlReader
{
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @var PackageManager
*/
protected $packageManager;
/**
* @throws \InvalidArgumentException
*/
public function __construct(EventDispatcherInterface $eventDispatcher, PackageManager $packageManager)
{
$this->eventDispatcher = $eventDispatcher;
$this->packageManager = $packageManager;
}
/**
* Cycle through all loaded extensions and get full table definitions as concatenated string
*
* @param bool $withStatic TRUE if sql from ext_tables_static+adt.sql should be loaded, too.
* @return string Concatenated SQL of loaded extensions ext_tables.sql
*/
public function getTablesDefinitionString(bool $withStatic = false): string
{
$sqlString = [];
// Find all ext_tables.sql of loaded extensions
foreach ($this->packageManager->getActivePackages() as $package) {
$packagePath = $package->getPackagePath();
if (@file_exists($packagePath . 'ext_tables.sql')) {
$sqlString[] = (string)file_get_contents($packagePath . 'ext_tables.sql');
}
if ($withStatic && @file_exists($packagePath . 'ext_tables_static+adt.sql')) {
$sqlString[] = (string)file_get_contents($packagePath . 'ext_tables_static+adt.sql');
}
}
$event = $this->eventDispatcher->dispatch(new AlterTableDefinitionStatementsEvent($sqlString));
$sqlString = $event->getSqlData();
return implode(LF . LF, $sqlString);
}
/**
* Returns an array where every entry is a single SQL-statement.
* Input must be formatted like an ordinary MySQL dump file. Every statements needs to be terminated by a ';'
* and there may only be one statement (or partial statement) per line.
*
* @param string $dumpContent The SQL dump content.
* @param string|null $queryRegex Regex to select which statements to return.
* @return array Array of SQL statements
*/
public function getStatementArray(string $dumpContent, ?string $queryRegex = null): array
{
$statementArray = [];
$statementArrayPointer = 0;
$isInMultilineComment = false;
foreach (explode(LF, $dumpContent) as $lineContent) {
$lineContent = trim($lineContent);
// Skip empty lines and comments
if ($lineContent === ''
|| $lineContent[0] === '#'
|| str_starts_with($lineContent, '--')
|| str_starts_with($lineContent, '/*')
|| str_ends_with($lineContent, '*/')
|| $isInMultilineComment
) {
// skip c style multiline comments
if (str_starts_with($lineContent, '/*') && !str_ends_with($lineContent, '*/')) {
$isInMultilineComment = true;
}
if (str_ends_with($lineContent, '*/')) {
$isInMultilineComment = false;
}
continue;
}
$statementArray[$statementArrayPointer] = ($statementArray[$statementArrayPointer] ?? '') . $lineContent;
if (str_ends_with($lineContent, ';')) {
$statement = trim($statementArray[$statementArrayPointer]);
if (!$statement || ($queryRegex && !preg_match('/' . $queryRegex . '/i', $statement))) {
unset($statementArray[$statementArrayPointer]);
}
$statementArrayPointer++;
} else {
$statementArray[$statementArrayPointer] .= ' ';
}
}
return $statementArray;
}
/**
* Extract only INSERT statements from SQL dump
*/
public function getInsertStatementArray(string $dumpContent): array
{
return $this->getStatementArray($dumpContent, '^INSERT');
}
/**
* Extract only CREATE TABLE statements from SQL dump
*/
public function getCreateTableStatementArray(string $dumpContent): array
{
return $this->getStatementArray($dumpContent, '^CREATE TABLE');
}
}
+342
View File
@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Exception\InvalidState;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff as DoctrineTableDiff;
/**
* Based on the doctrine/dbal implementation restoring direct property access
* and adding further helper methods.
*
* @internal not part of public core API.
*/
class TableDiff extends DoctrineTableDiff
{
/**
* $newTableName is a TYPO3 internal addition to handle renames at a later point.
*/
public ?string $newName = null;
/**
* Constructs a TableDiff object.
*
* @param array<string, Column> $addedColumns
* @param array<string, ColumnDiff> $changedColumns
* @param array<string, Column> $droppedColumns
* @param array<string, Index> $addedIndexes
* @param array<string, Index> $modifiedIndexes
* @param array<string, Index> $droppedIndexes
* @param array<string, Index> $renamedIndexes
* @param array<ForeignKeyConstraint> $addedForeignKeys
* @param array<ForeignKeyConstraint> $modifiedForeignKeys
* @param array<ForeignKeyConstraint> $droppedForeignKeys
* @param array<int|string, mixed> $tableOptions
*
* @internal The diff can be only instantiated by a {@see Comparator}.
*
* @todo Consider to change from array to typed collections with array access support.
*/
public function __construct(
public Table $oldTable,
public array $addedColumns = [],
public array $changedColumns = [],
public array $droppedColumns = [],
public array $addedIndexes = [],
public array $modifiedIndexes = [],
public array $droppedIndexes = [],
public array $renamedIndexes = [],
public array $addedForeignKeys = [],
public array $modifiedForeignKeys = [],
public array $droppedForeignKeys = [],
public array $tableOptions = [],
) {
// NOTE: parent::__construct() not called by intention.
}
/**
* Getter for table options.
*
* @return array<int|string, mixed>
*/
public function getTableOptions(): array
{
return $this->tableOptions;
}
/**
* Setter for table options
*
* @param array<int|string, mixed> $tableOptions
*/
public function setTableOptions(array $tableOptions): self
{
$this->tableOptions = $tableOptions;
return $this;
}
/**
* Check if a table options has been set.
*/
public function hasTableOption(string $optionName): bool
{
return array_key_exists($optionName, $this->tableOptions);
}
public function getTableOption(string $optionName): string
{
if ($this->hasTableOption($optionName)) {
return (string)$this->tableOptions[$optionName];
}
return '';
}
public function getOldTable(): Table
{
return $this->oldTable;
}
/** @return array<string, Column> */
public function getAddedColumns(): array
{
return $this->addedColumns;
}
/** @return array<string, ColumnDiff> */
public function getChangedColumns(): array
{
return $this->changedColumns;
}
/** @return array<string, Column> */
public function getDroppedColumns(): array
{
return $this->droppedColumns;
}
/** @return array<string, Index> */
public function getAddedIndexes(): array
{
return $this->addedIndexes;
}
/**
* @deprecated Use {@see getAddedIndexes()} and {@see getDroppedIndexes()} instead.
*
* @return array<string, Index>
*/
public function getModifiedIndexes(): array
{
return $this->modifiedIndexes;
}
/** @return array<string, Index> */
public function getDroppedIndexes(): array
{
return $this->droppedIndexes;
}
/** @return array<string,Index> */
public function getRenamedIndexes(): array
{
return $this->renamedIndexes;
}
/** @return array<ForeignKeyConstraint> */
public function getAddedForeignKeys(): array
{
return $this->addedForeignKeys;
}
/**
* @deprecated Use {@see getAddedForeignKeys()} and {@see getDroppedForeignKeys()} instead.
*
* @return array<ForeignKeyConstraint>
*/
public function getModifiedForeignKeys(): array
{
return $this->modifiedForeignKeys;
}
/**
* @deprecated Use {@see getDroppedForeignKeyConstraintNames()}.
*
* @return array<ForeignKeyConstraint>
*/
public function getDroppedForeignKeys(): array
{
return $this->droppedForeignKeys;
}
/**
* Overridden, because the parent implementation reads the parent property directly. As the parent
* constructor is not called by intention, that property is never initialized, making the inherited
* method raise an `Error`. Reads the redeclared property here instead, keeping the parent behaviour.
*
* @return array<UnqualifiedName>
*/
public function getDroppedForeignKeyConstraintNames(): array
{
$names = [];
foreach ($this->droppedForeignKeys as $droppedForeignKey) {
$name = $droppedForeignKey->getObjectName();
if ($name === null) {
throw InvalidState::tableDiffContainsUnnamedDroppedForeignKeyConstraints();
}
$names[] = $name;
}
return $names;
}
public function isEmpty(): bool
{
return count($this->getAddedColumns()) === 0
&& count($this->getChangedColumns()) === 0
&& count($this->getDroppedColumns()) === 0
&& count($this->getAddedIndexes()) === 0
&& count($this->getModifiedIndexes()) === 0
&& count($this->getDroppedIndexes()) === 0
&& count($this->getRenamedIndexes()) === 0
&& count($this->getAddedForeignKeys()) === 0
&& count($this->getModifiedForeignKeys()) === 0
&& count($this->getDroppedForeignKeys()) === 0
// doctrine/dbal 4.x removed the newName. TYPO3 needs that to provide a rename to prefix logic before
// really dropping tables instead. Therefore, we need to add here an empty check for the reintroduced
// property.See for example: ConnectionMigrator->migrateUnprefixedRemovedTablesToRenames
&& $this->getNewName() !== null && $this->getNewName() !== ''
&& $this->getTableOptions() === [];
}
public function getNewName(): ?string
{
return $this->newName;
}
public static function ensure(DoctrineTableDiff|TableDiff $tableDiff): self
{
$diff = new self(
// oldTable
$tableDiff->getOldTable(),
// addedColumns
$tableDiff->getAddedColumns(),
// changedColumns
[],
// droppedColumns
$tableDiff->getDroppedColumns(),
// addedIndexes
$tableDiff->getAddedIndexes(),
// modifiedIndexes
[],
// droppedIndexes
$tableDiff->getDroppedIndexes(),
// renamedIndexes
$tableDiff->getRenamedIndexes(),
// addedForeignKeys
$tableDiff->getAddedForeignKeys(),
// modifiedForeignKeys
$tableDiff->getModifiedForeignKeys(),
// droppedForeignKeys
$tableDiff->getDroppedForeignKeys(),
// tableOptions
($tableDiff instanceof TableDiff ? $tableDiff->tableOptions : []),
);
// doctrine/dbal 4+ removed the column name as array index for modified column definitions,
// but we rely on it. Restore it !
// Ensure to use custom ColumnDiff instance with more data and
foreach ($tableDiff->getChangedColumns() as $changedColumn) {
$diff->changedColumns[$changedColumn->getOldColumn()->getName()] = new ColumnDiff(
// oldColumn
$changedColumn->getOldColumn(),
// newColumn
$changedColumn->getNewColumn(),
);
}
// doctrine/dbal 4+ removed the index name as array index for modified index definitions,
// but we rely on it. Restore it !.
foreach ($tableDiff->getModifiedIndexes() as $modifiedIndex) {
$diff->modifiedIndexes[$modifiedIndex->getName()] = $modifiedIndex;
}
// Accumulate modified index separated into added and dropped information to modifiedIndexes again,
// otherwise required drop action may not be executed before trying to add an existing index first.
// Required for planned doctrine/dbal 4.3.0 change (deprecation) and currently breaking with an open
// discussion to mitigate that before dbal release. We still prepare for this case to be on the safer
// side here.
// Needs to be done in a two-step strategy to avoid changing array while iterating over it.
// - https://github.com/doctrine/dbal/pull/6831
// - https://github.com/doctrine/dbal/issues/6880
/**
* @var array<int, array{added: Index, dropped: Index}> $transformIndexOperations
*/
$transformIndexOperations = [];
foreach ($diff->getAddedIndexes() as $addedIndex) {
foreach ($diff->getDroppedIndexes() as $droppedIndex) {
if ($droppedIndex->getName() === $addedIndex->getName()) {
$transformIndexOperations[] = [
'added' => $addedIndex,
'dropped' => $droppedIndex,
];
}
}
}
foreach ($transformIndexOperations as $data) {
$diff->unsetAddedIndex($data['added']);
$diff->unsetDroppedIndex($data['dropped']);
$diff->modifiedIndexes[$data['added']->getName()] = $data['added'];
}
return $diff;
}
/**
* @internal This method exists only for compatibility with the current implementation of schema managers
* that modify the diff while processing it.
*/
public function unsetAddedIndex(Index $index): void
{
$this->addedIndexes = array_filter(
$this->addedIndexes,
static function (Index $addedIndex) use ($index): bool {
return $addedIndex !== $index;
},
);
}
/**
* @internal This method exists only for compatibility with the current implementation of schema managers
* that modify the diff while processing it.
*/
public function unsetDroppedIndex(Index $index): void
{
$this->droppedIndexes = array_filter(
$this->droppedIndexes,
static function (Index $droppedIndex) use ($index): bool {
return $droppedIndex !== $index;
},
);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform as DoctrineAbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
/**
* This custom type extends doctrine native DateTimeType to allow a
* formatted string (in "Y-m-d H:i:s") directly, in addition to a DateTimeInterface.
*
* @internal not part of public core API.
*/
class DateTimeType extends \Doctrine\DBAL\Types\DateTimeType
{
public function convertToDatabaseValue($value, DoctrineAbstractPlatform $platform): ?string
{
if ($value === null || (is_string($value) && $value !== '')) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format($platform->getDateTimeFormatString());
}
throw InvalidType::new($value, self::getTypeRegistry()->lookupName($this), ['null', 'string', 'DateTime']);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform as DoctrineAbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
/**
* This custom type extends doctrine native DateType to allow a
* formatted string (in "Y-m-d") directly, in addition to a DateTimeInterface.
*
* @internal not part of public core API.
*/
class DateType extends \Doctrine\DBAL\Types\DateType
{
public function convertToDatabaseValue($value, DoctrineAbstractPlatform $platform): mixed
{
if ($value === null || (is_string($value) && $value !== '')) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format($platform->getDateFormatString());
}
throw InvalidType::new($value, self::getTypeRegistry()->lookupName($this), ['null', 'string', 'DateTime']);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
/**
* Type that maps a TYPE field.
*
* @internal not part of public core API.
*
* @todo SetType does not work for SQLite and PostgresSQL. SQLite supports it with a slightly other syntax and
* PostgreSQL needs to create a custom type with a human-readable name, which is not reasonable either. Consider
* to deprecate and drop ENUM support due not having compatibility for all supported database systems.
*/
class SetType extends Type
{
public const TYPE = 'set';
/**
* Gets the SQL declaration snippet for a field of this type.
*
* @param array $fieldDeclaration The field declaration.
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform The currently used database platform.
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
if (method_exists($platform, 'getSetDeclarationSQL')) {
return $platform->getSetDeclarationSQL($fieldDeclaration);
}
$quotedValues = array_map($platform->quoteStringLiteral(...), $fieldDeclaration['values']);
return sprintf('SET(%s)', implode(', ', $quotedValues));
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Database\Schema\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform as DoctrineAbstractPlatform;
use Doctrine\DBAL\Types\Exception\InvalidType;
/**
* This custom type extends doctrine native TimeType to allow a
* formatted string (in "H:i:s") directly, in addition to a DateTimeInterface.
*
* @internal not part of public core API.
*/
class TimeType extends \Doctrine\DBAL\Types\TimeType
{
public function convertToDatabaseValue($value, DoctrineAbstractPlatform $platform): ?string
{
if ($value === null || (is_string($value) && $value !== '')) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format($platform->getTimeFormatString());
}
throw InvalidType::new($value, self::getTypeRegistry()->lookupName($this), ['null', 'DateTime']);
}
}