recreate project
This commit is contained in:
+422
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\NotImplemented;
|
||||
use Doctrine\DBAL\Schema\Name\GenericName;
|
||||
use Doctrine\DBAL\Schema\Name\Identifier;
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Throwable;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function crc32;
|
||||
use function dechex;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* The abstract asset allows to reset the name of all assets without publishing this to the public userland.
|
||||
*
|
||||
* This encapsulation hack is necessary to keep a consistent state of the database schema. Say we have a list of tables
|
||||
* array($tableName => Table($tableName)); if you want to rename the table, you have to make sure this does not get
|
||||
* recreated during schema migration.
|
||||
*
|
||||
* @internal This class should be extended only by DBAL itself.
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
abstract class AbstractAsset
|
||||
{
|
||||
protected string $_name = '';
|
||||
|
||||
/**
|
||||
* Indicates whether the object name has been initialized.
|
||||
*/
|
||||
protected bool $isNameInitialized = false;
|
||||
|
||||
/**
|
||||
* Namespace of the asset. If none isset the default namespace is assumed.
|
||||
*
|
||||
* @deprecated Use {@see NamedObject::getObjectName()} and {@see OptionallyQualifiedName::getQualifier()} instead.
|
||||
*/
|
||||
protected ?string $_namespace = null;
|
||||
|
||||
protected bool $_quoted = false;
|
||||
|
||||
/** @var list<Identifier> */
|
||||
private array $identifiers = [];
|
||||
|
||||
private bool $validateFuture = false;
|
||||
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
if ($name === null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6610',
|
||||
'Not passing $name to %s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_setName($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for parsing the object name.
|
||||
*
|
||||
* @deprecated Parse the name in the constructor instead.
|
||||
*
|
||||
* @return Parser<N>
|
||||
*/
|
||||
protected function getNameParser(): Parser
|
||||
{
|
||||
throw NotImplemented::fromMethod(static::class, __FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the object name.
|
||||
*
|
||||
* @deprecated Set the name in the constructor instead.
|
||||
*
|
||||
* @param ?N $name
|
||||
*/
|
||||
protected function setName(?Name $name): void
|
||||
{
|
||||
throw NotImplemented::fromMethod(static::class, __FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of this asset.
|
||||
*
|
||||
* @deprecated Use the constructor instead.
|
||||
*/
|
||||
protected function _setName(string $name): void
|
||||
{
|
||||
$this->isNameInitialized = false;
|
||||
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6610',
|
||||
'%s is deprecated. Use the constructor instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$input = $name;
|
||||
|
||||
if ($this->isIdentifierQuoted($name)) {
|
||||
$this->_quoted = true;
|
||||
$name = $this->trimQuotes($name);
|
||||
}
|
||||
|
||||
if (str_contains($name, '.')) {
|
||||
$parts = explode('.', $name);
|
||||
$this->_namespace = $parts[0];
|
||||
$name = $parts[1];
|
||||
}
|
||||
|
||||
$this->_name = $name;
|
||||
|
||||
$this->validateFuture = false;
|
||||
|
||||
if ($input !== '') {
|
||||
try {
|
||||
$parsedName = $this->getNameParser()->parse($input);
|
||||
} catch (Throwable $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6592',
|
||||
'Unable to parse object name: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$parsedName = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->setName($parsedName);
|
||||
} catch (Throwable $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6646',
|
||||
'Using invalid database object names is deprecated: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isNameInitialized = true;
|
||||
|
||||
if ($parsedName === null) {
|
||||
$this->identifiers = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parsedName instanceof UnqualifiedName) {
|
||||
$identifiers = [$parsedName->getIdentifier()];
|
||||
} elseif ($parsedName instanceof OptionallyQualifiedName) {
|
||||
$unqualifiedName = $parsedName->getUnqualifiedName();
|
||||
$qualifier = $parsedName->getQualifier();
|
||||
|
||||
$identifiers = $qualifier !== null
|
||||
? [$qualifier, $unqualifiedName]
|
||||
: [$unqualifiedName];
|
||||
} elseif ($parsedName instanceof GenericName) {
|
||||
$identifiers = $parsedName->getIdentifiers();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (count($identifiers)) {
|
||||
case 1:
|
||||
$namespace = null;
|
||||
$name = $identifiers[0];
|
||||
break;
|
||||
|
||||
case 2:
|
||||
[$namespace, $name] = $identifiers;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$this->identifiers = $identifiers;
|
||||
$this->validateFuture = true;
|
||||
|
||||
$futureName = $name->getValue();
|
||||
$futureNamespace = $namespace?->getValue();
|
||||
|
||||
if ($this->_name !== $futureName) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6592',
|
||||
'Instead of "%s", this name will be interpreted as "%s" in 5.0',
|
||||
$this->_name,
|
||||
$futureName,
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->_namespace === $futureNamespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6592',
|
||||
'Instead of %s, the namespace in this name will be interpreted as %s in 5.0.',
|
||||
$this->_namespace !== null ? sprintf('"%s"', $this->_namespace) : 'null',
|
||||
$futureNamespace !== null ? sprintf('"%s"', $futureNamespace) : 'null',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this asset in the default namespace?
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function isInDefaultNamespace(string $defaultNamespaceName): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6664',
|
||||
'%s is deprecated and will be removed in 5.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->_namespace === $defaultNamespaceName || $this->_namespace === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace name of this asset.
|
||||
*
|
||||
* If NULL is returned this means the default namespace is used.
|
||||
*
|
||||
* @deprecated Use {@see NamedObject::getObjectName()} and {@see OptionallyQualifiedName::getQualifier()} instead.
|
||||
*/
|
||||
public function getNamespaceName(): ?string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6664',
|
||||
'%s is deprecated and will be removed in 5.0. Use NamedObject::getObjectName()'
|
||||
. ' and OptionallyQualifiedName::getQualifier() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->_namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* The shortest name is stripped of the default namespace. All other
|
||||
* namespaced elements are returned as full-qualified names.
|
||||
*
|
||||
* @deprecated Use {@link getName()} instead.
|
||||
*/
|
||||
public function getShortestName(?string $defaultNamespaceName): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6657',
|
||||
'%s is deprecated and will be removed in 5.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$shortestName = $this->getName();
|
||||
if ($this->_namespace === $defaultNamespaceName) {
|
||||
$shortestName = $this->_name;
|
||||
}
|
||||
|
||||
return strtolower($shortestName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this asset's name is quoted.
|
||||
*/
|
||||
public function isQuoted(): bool
|
||||
{
|
||||
return $this->_quoted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this identifier is quoted.
|
||||
*
|
||||
* @deprecated Parse the name and introspect its identifiers individually using {@see Identifier::isQuoted()}
|
||||
* instead.
|
||||
*/
|
||||
protected function isIdentifierQuoted(string $identifier): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6677',
|
||||
'%s is deprecated and will be removed in 5.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim quotes from the identifier.
|
||||
*/
|
||||
protected function trimQuotes(string $identifier): string
|
||||
{
|
||||
return str_replace(['`', '"', '[', ']'], '', $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this schema asset.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
if ($this->_namespace !== null) {
|
||||
return $this->_namespace . '.' . $this->_name;
|
||||
}
|
||||
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the quoted representation of this asset but only if it was defined with one. Otherwise
|
||||
* return the plain unquoted value as inserted.
|
||||
*
|
||||
* @deprecated Use {@see NamedObject::getObjectName()} or {@see OptionallyQualifiedName::getObjectName()} followed
|
||||
* by {@see Name::toSQL()} instead.
|
||||
*/
|
||||
public function getQuotedName(AbstractPlatform $platform): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6674',
|
||||
'%s is deprecated and will be removed in 5.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$keywords = $platform->getReservedKeywordsList();
|
||||
$folding = $platform->getUnquotedIdentifierFolding();
|
||||
$parts = $normalizedParts = [];
|
||||
|
||||
foreach (explode('.', $this->getName()) as $identifier) {
|
||||
$isQuoted = $this->_quoted || $keywords->isKeyword($identifier);
|
||||
|
||||
if (! $isQuoted) {
|
||||
$parts[] = $identifier;
|
||||
|
||||
/** @phpstan-ignore argument.type */
|
||||
$normalizedParts[] = $folding->foldUnquotedIdentifier($identifier);
|
||||
} else {
|
||||
$parts[] = $platform->quoteSingleIdentifier($identifier);
|
||||
$normalizedParts[] = $identifier;
|
||||
}
|
||||
}
|
||||
|
||||
$name = implode('.', $parts);
|
||||
|
||||
if ($this->validateFuture) {
|
||||
$futureParts = array_map(static function (Identifier $identifier) use ($folding): string {
|
||||
$value = $identifier->getValue();
|
||||
|
||||
if (! $identifier->isQuoted()) {
|
||||
$value = $folding->foldUnquotedIdentifier($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $this->identifiers);
|
||||
|
||||
if ($normalizedParts !== $futureParts) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6592',
|
||||
'Relying on implicitly quoted identifiers preserving their original case is deprecated. '
|
||||
. 'The current name %s will become %s in 5.0. '
|
||||
. 'Please quote the name if the case needs to be preserved.',
|
||||
$name,
|
||||
implode('.', array_map([$platform, 'quoteSingleIdentifier'], $futureParts)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an identifier from a list of column names obeying a certain string length.
|
||||
*
|
||||
* This is especially important for Oracle, since it does not allow identifiers larger than 30 chars,
|
||||
* however building idents automatically for foreign keys, composite keys or such can easily create
|
||||
* very long names.
|
||||
*
|
||||
* @param array<int, string> $columnNames
|
||||
* @param positive-int $maxSize
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
protected function _generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string
|
||||
{
|
||||
$hash = implode('', array_map(static function ($column): string {
|
||||
return dechex(crc32($column));
|
||||
}, $columnNames));
|
||||
|
||||
return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidName;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidState;
|
||||
|
||||
/**
|
||||
* An abstract {@see NamedObject}.
|
||||
*
|
||||
* @template N of Name
|
||||
* @extends AbstractAsset<N>
|
||||
* @implements NamedObject<N>
|
||||
*/
|
||||
abstract class AbstractNamedObject extends AbstractAsset implements NamedObject
|
||||
{
|
||||
/**
|
||||
* The name of the database object.
|
||||
*
|
||||
* Until the validity of the name is enforced, this property isn't guaranteed to be always initialized. The property
|
||||
* can be accessed only if {@see $isNameInitialized} is set to true.
|
||||
*
|
||||
* @var N
|
||||
*/
|
||||
protected Name $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object name.
|
||||
*
|
||||
* @return N
|
||||
*
|
||||
* @throws InvalidState
|
||||
*/
|
||||
public function getObjectName(): Name
|
||||
{
|
||||
if (! $this->isNameInitialized) {
|
||||
throw InvalidState::objectNameNotInitialized();
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
protected function setName(?Name $name): void
|
||||
{
|
||||
if ($name === null) {
|
||||
throw InvalidName::fromEmpty();
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidState;
|
||||
|
||||
/**
|
||||
* An abstract {@see OptionallyNamedObject}.
|
||||
*
|
||||
* @template N of Name
|
||||
* @extends AbstractAsset<N>
|
||||
* @implements OptionallyNamedObject<N>
|
||||
*/
|
||||
abstract class AbstractOptionallyNamedObject extends AbstractAsset implements OptionallyNamedObject
|
||||
{
|
||||
/**
|
||||
* The name of the database object.
|
||||
*
|
||||
* Until the validity of the name is enforced, this property isn't guaranteed to be always initialized. The property
|
||||
* can be accessed only if {@see $isNameInitialized} is set to true.
|
||||
*
|
||||
* @var ?N
|
||||
*/
|
||||
protected ?Name $name;
|
||||
|
||||
public function __construct(?string $name)
|
||||
{
|
||||
parent::__construct($name ?? '');
|
||||
}
|
||||
|
||||
public function getObjectName(): ?Name
|
||||
{
|
||||
if (! $this->isNameInitialized) {
|
||||
throw InvalidState::objectNameNotInitialized();
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
protected function setName(?Name $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
|
||||
/** @internal */
|
||||
interface Exception extends SchemaException
|
||||
{
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class ObjectAlreadyExists extends LogicException implements Exception
|
||||
{
|
||||
public function __construct(string $message, private readonly UnqualifiedName $objectName)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
public function getObjectName(): UnqualifiedName
|
||||
{
|
||||
return $this->objectName;
|
||||
}
|
||||
|
||||
public static function new(UnqualifiedName $objectName): self
|
||||
{
|
||||
return new self(sprintf('Object %s already exists.', $objectName->toString()), $objectName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class ObjectDoesNotExist extends LogicException implements Exception
|
||||
{
|
||||
public function __construct(string $message, private readonly UnqualifiedName $objectName)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
public function getObjectName(): UnqualifiedName
|
||||
{
|
||||
return $this->objectName;
|
||||
}
|
||||
|
||||
public static function new(UnqualifiedName $objectName): self
|
||||
{
|
||||
return new self(sprintf('Object %s does not exist.', $objectName->toString()), $objectName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
/**
|
||||
* A set of objects where each object is uniquely identified by its {@link UnqualifiedName}.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @template E of object
|
||||
*/
|
||||
interface ObjectSet
|
||||
{
|
||||
/**
|
||||
* Checks if the set is empty.
|
||||
*/
|
||||
public function isEmpty(): bool;
|
||||
|
||||
/**
|
||||
* Returns the element with the given name. If no such element exists, null is returned.
|
||||
*
|
||||
* @phpstan-return E|null
|
||||
*/
|
||||
public function get(UnqualifiedName $elementName): ?object;
|
||||
|
||||
/**
|
||||
* Adds the given element to the set.
|
||||
*
|
||||
* @phpstan-param E $element
|
||||
*
|
||||
* @throws ObjectAlreadyExists If an element with the same name already exists.
|
||||
*/
|
||||
public function add(object $element): void;
|
||||
|
||||
/**
|
||||
* Removes the element with the given name from the set.
|
||||
*
|
||||
* @throws ObjectDoesNotExist If no element with the given name exists.
|
||||
*/
|
||||
public function remove(UnqualifiedName $elementName): void;
|
||||
|
||||
/**
|
||||
* Modifies the element with the given name using the provided callable.
|
||||
*
|
||||
* @param callable(E): E $modification
|
||||
*
|
||||
* @throws ObjectDoesNotExist If no element with the given name exists.
|
||||
* @throws ObjectAlreadyExists If an element with the name after modification already exists.
|
||||
*/
|
||||
public function modify(UnqualifiedName $elementName, callable $modification): void;
|
||||
|
||||
/**
|
||||
* Clears the set, removing all elements.
|
||||
*/
|
||||
public function clear(): void;
|
||||
|
||||
/**
|
||||
* Returns the elements of the set represented as a list.
|
||||
*
|
||||
* @phpstan-return list<E>
|
||||
*/
|
||||
public function toList(): array;
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\OptionallyNamedObject;
|
||||
|
||||
use function array_splice;
|
||||
use function count;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* An ordered set of {@link OptionallyNamedObject}s with names being {@link UnqualifiedName}.
|
||||
*
|
||||
* New objects are added to the end of the set. The order of elements is preserved during modification.
|
||||
*
|
||||
* If an object is unnamed, it can be added to the set but cannot be referenced and, therefore, modified or removed
|
||||
* from the set. Two unnamed objects are considered as having different names.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @template E of OptionallyNamedObject<UnqualifiedName>
|
||||
* @template-implements ObjectSet<E>
|
||||
*/
|
||||
final class OptionallyUnqualifiedNamedObjectSet implements ObjectSet
|
||||
{
|
||||
/** @var list<E> */
|
||||
private array $elements = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
private array $elementPositionsByKey = [];
|
||||
|
||||
/** @phpstan-param E ...$elements */
|
||||
public function __construct(OptionallyNamedObject ...$elements)
|
||||
{
|
||||
foreach ($elements as $element) {
|
||||
$this->add($element);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return count($this->elements) === 0;
|
||||
}
|
||||
|
||||
public function get(UnqualifiedName $elementName): ?OptionallyNamedObject
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (isset($this->elementPositionsByKey[$key])) {
|
||||
return $this->elements[$this->elementPositionsByKey[$key]];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function add(object $element): void
|
||||
{
|
||||
$elementName = $element->getObjectName();
|
||||
|
||||
if ($elementName !== null) {
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (isset($this->elementPositionsByKey[$key])) {
|
||||
throw ObjectAlreadyExists::new($elementName);
|
||||
}
|
||||
|
||||
$this->elementPositionsByKey[$key] = count($this->elements);
|
||||
}
|
||||
|
||||
$this->elements[] = $element;
|
||||
}
|
||||
|
||||
public function remove(UnqualifiedName $elementName): void
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (! isset($this->elementPositionsByKey[$key])) {
|
||||
throw ObjectDoesNotExist::new($elementName);
|
||||
}
|
||||
|
||||
$this->removeByKey($key);
|
||||
}
|
||||
|
||||
public function modify(UnqualifiedName $elementName, callable $modification): void
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (! isset($this->elementPositionsByKey[$key])) {
|
||||
throw ObjectDoesNotExist::new($elementName);
|
||||
}
|
||||
|
||||
$position = $this->elementPositionsByKey[$key];
|
||||
|
||||
$this->replace($key, $position, $modification($this->elements[$position]));
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->elements = $this->elementPositionsByKey = [];
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public function toList(): array
|
||||
{
|
||||
return $this->elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the element corresponding to the old key with the provided element.
|
||||
*
|
||||
* @phpstan-param E $element
|
||||
*
|
||||
* @throws ObjectAlreadyExists If an element with the same name as the element name already exists.
|
||||
*/
|
||||
private function replace(string $oldKey, int $position, OptionallyNamedObject $element): void
|
||||
{
|
||||
$elementName = $element->getObjectName();
|
||||
|
||||
if ($elementName !== null) {
|
||||
$newKey = $this->getKey($elementName);
|
||||
|
||||
if ($newKey !== $oldKey) {
|
||||
if (isset($this->elementPositionsByKey[$newKey])) {
|
||||
throw ObjectAlreadyExists::new($elementName);
|
||||
}
|
||||
|
||||
unset($this->elementPositionsByKey[$oldKey]);
|
||||
|
||||
$this->elementPositionsByKey[$newKey] = $position;
|
||||
}
|
||||
} else {
|
||||
unset($this->elementPositionsByKey[$oldKey]);
|
||||
}
|
||||
|
||||
// @phpstan-ignore assign.propertyType
|
||||
$this->elements[$position] = $element;
|
||||
}
|
||||
|
||||
private function removeByKey(string $key): void
|
||||
{
|
||||
$position = $this->elementPositionsByKey[$key];
|
||||
|
||||
array_splice($this->elements, $position, 1);
|
||||
unset($this->elementPositionsByKey[$key]);
|
||||
|
||||
foreach ($this->elementPositionsByKey as $elementKey => $elementPosition) {
|
||||
if ($elementPosition <= $position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->elementPositionsByKey[$elementKey]--;
|
||||
}
|
||||
}
|
||||
|
||||
private function getKey(UnqualifiedName $name): string
|
||||
{
|
||||
return strtolower($name->getIdentifier()->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Collections;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\NamedObject;
|
||||
|
||||
use function array_combine;
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* An ordered set of {@link NamedObject}s with names being {@link UnqualifiedName}.
|
||||
*
|
||||
* New objects are added to the end of the set. The order of elements is preserved during modification.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @template E of NamedObject<UnqualifiedName>
|
||||
* @template-implements ObjectSet<E>
|
||||
*/
|
||||
final class UnqualifiedNamedObjectSet implements ObjectSet
|
||||
{
|
||||
/** @var array<string, E> */
|
||||
private array $elements = [];
|
||||
|
||||
/** @phpstan-param E ...$elements */
|
||||
public function __construct(NamedObject ...$elements)
|
||||
{
|
||||
foreach ($elements as $element) {
|
||||
$this->add($element);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return count($this->elements) === 0;
|
||||
}
|
||||
|
||||
public function get(UnqualifiedName $elementName): ?NamedObject
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
return $this->elements[$key] ?? null;
|
||||
}
|
||||
|
||||
public function add(object $element): void
|
||||
{
|
||||
$elementName = $element->getObjectName();
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (isset($this->elements[$key])) {
|
||||
throw ObjectAlreadyExists::new($elementName);
|
||||
}
|
||||
|
||||
$this->elements[$key] = $element;
|
||||
}
|
||||
|
||||
public function remove(UnqualifiedName $elementName): void
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (! isset($this->elements[$key])) {
|
||||
throw ObjectDoesNotExist::new($elementName);
|
||||
}
|
||||
|
||||
unset($this->elements[$key]);
|
||||
}
|
||||
|
||||
public function modify(UnqualifiedName $elementName, callable $modification): void
|
||||
{
|
||||
$key = $this->getKey($elementName);
|
||||
|
||||
if (! isset($this->elements[$key])) {
|
||||
throw ObjectDoesNotExist::new($elementName);
|
||||
}
|
||||
|
||||
$this->replace($key, $modification($this->elements[$key]));
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->elements = [];
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public function toList(): array
|
||||
{
|
||||
return array_values($this->elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the element corresponding to the old key with the provided element. The position of the element in the
|
||||
* set is preserved.
|
||||
*
|
||||
* @phpstan-param E $element
|
||||
*
|
||||
* @throws ObjectAlreadyExists If an element with the same name as the element name already exists.
|
||||
*/
|
||||
private function replace(string $oldKey, NamedObject $element): void
|
||||
{
|
||||
$elementName = $element->getObjectName();
|
||||
$newKey = $this->getKey($elementName);
|
||||
|
||||
if ($newKey === $oldKey) {
|
||||
$this->elements[$oldKey] = $element;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->elements[$newKey])) {
|
||||
throw ObjectAlreadyExists::new($elementName);
|
||||
}
|
||||
|
||||
$keys = array_keys($this->elements);
|
||||
$values = array_values($this->elements);
|
||||
|
||||
$position = array_search($oldKey, $keys, true);
|
||||
assert($position !== false);
|
||||
|
||||
$keys[$position] = $newKey;
|
||||
$values[$position] = $element;
|
||||
|
||||
$this->elements = array_combine($keys, $values);
|
||||
}
|
||||
|
||||
private function getKey(UnqualifiedName $name): string
|
||||
{
|
||||
return strtolower($name->getIdentifier()->getValue());
|
||||
}
|
||||
}
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\UnknownColumnOption;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function array_merge;
|
||||
use function method_exists;
|
||||
|
||||
/**
|
||||
* Object representation of a database column.
|
||||
*
|
||||
* @final
|
||||
* @extends AbstractNamedObject<UnqualifiedName>
|
||||
* @phpstan-type ColumnProperties = array{
|
||||
* name: string,
|
||||
* type: Type,
|
||||
* default: mixed,
|
||||
* notnull?: bool,
|
||||
* autoincrement: bool,
|
||||
* columnDefinition: ?non-empty-string,
|
||||
* comment: string,
|
||||
* charset?: ?non-empty-string,
|
||||
* collation?: ?non-empty-string,
|
||||
* }
|
||||
* @phpstan-type PlatformOptions = array{
|
||||
* charset?: ?non-empty-string,
|
||||
* collation?: ?non-empty-string,
|
||||
* default_constraint_name?: non-empty-string,
|
||||
* jsonb?: bool,
|
||||
* version?: bool,
|
||||
* }
|
||||
*/
|
||||
class Column extends AbstractNamedObject
|
||||
{
|
||||
protected Type $_type;
|
||||
|
||||
protected ?int $_length = null;
|
||||
|
||||
protected ?int $_precision = null;
|
||||
|
||||
protected int $_scale = 0;
|
||||
|
||||
protected bool $_unsigned = false;
|
||||
|
||||
protected bool $_fixed = false;
|
||||
|
||||
protected bool $_notnull = true;
|
||||
|
||||
protected mixed $_default = null;
|
||||
|
||||
protected bool $_autoincrement = false;
|
||||
|
||||
/** @var list<string> */
|
||||
protected array $_values = [];
|
||||
|
||||
/** @var PlatformOptions */
|
||||
protected array $_platformOptions = [];
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
protected ?string $_columnDefinition = null;
|
||||
|
||||
protected string $_comment = '';
|
||||
|
||||
/**
|
||||
* @internal Use {@link Column::editor()} to instantiate an editor and {@link ColumnEditor::create()} to create a
|
||||
* column.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(string $name, Type $type, array $options = [])
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->setType($type);
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
protected function getNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return Parsers::getUnqualifiedNameParser();
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
public function setOptions(array $options): self
|
||||
{
|
||||
foreach ($options as $name => $value) {
|
||||
$method = 'set' . $name;
|
||||
|
||||
if (! method_exists($this, $method)) {
|
||||
throw UnknownColumnOption::new($name);
|
||||
}
|
||||
|
||||
$this->$method($value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(Type $type): self
|
||||
{
|
||||
$this->_type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLength(?int $length): self
|
||||
{
|
||||
$this->_length = $length;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPrecision(?int $precision): self
|
||||
{
|
||||
$this->_precision = $precision;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setScale(int $scale): self
|
||||
{
|
||||
$this->_scale = $scale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUnsigned(bool $unsigned): self
|
||||
{
|
||||
$this->_unsigned = $unsigned;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFixed(bool $fixed): self
|
||||
{
|
||||
$this->_fixed = $fixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setNotnull(bool $notnull): self
|
||||
{
|
||||
$this->_notnull = $notnull;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDefault(mixed $default): self
|
||||
{
|
||||
$this->_default = $default;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param PlatformOptions $platformOptions */
|
||||
public function setPlatformOptions(array $platformOptions): self
|
||||
{
|
||||
if (isset($platformOptions['jsonb']) && $platformOptions['jsonb']) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6939',
|
||||
'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_platformOptions = $platformOptions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param key-of<PlatformOptions> $name */
|
||||
public function setPlatformOption(string $name, mixed $value): self
|
||||
{
|
||||
if ($name === 'jsonb' && (bool) $value === true) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6939',
|
||||
'The "jsonb" column platform option is deprecated. Use the "JSONB" type instead.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_platformOptions[$name] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param ?non-empty-string $value */
|
||||
public function setColumnDefinition(?string $value): self
|
||||
{
|
||||
$this->_columnDefinition = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): Type
|
||||
{
|
||||
return $this->_type;
|
||||
}
|
||||
|
||||
public function getLength(): ?int
|
||||
{
|
||||
return $this->_length;
|
||||
}
|
||||
|
||||
public function getPrecision(): ?int
|
||||
{
|
||||
return $this->_precision;
|
||||
}
|
||||
|
||||
public function getScale(): int
|
||||
{
|
||||
return $this->_scale;
|
||||
}
|
||||
|
||||
public function getUnsigned(): bool
|
||||
{
|
||||
return $this->_unsigned;
|
||||
}
|
||||
|
||||
public function getFixed(): bool
|
||||
{
|
||||
return $this->_fixed;
|
||||
}
|
||||
|
||||
public function getNotnull(): bool
|
||||
{
|
||||
return $this->_notnull;
|
||||
}
|
||||
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->_default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the character set to use with the column.
|
||||
*
|
||||
* @return ?non-empty-string
|
||||
*/
|
||||
public function getCharset(): ?string
|
||||
{
|
||||
return $this->_platformOptions['charset'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the collation to use with the column.
|
||||
*
|
||||
* @return ?non-empty-string
|
||||
*/
|
||||
public function getCollation(): ?string
|
||||
{
|
||||
return $this->_platformOptions['collation'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum value to enforce on the column.
|
||||
*/
|
||||
public function getMinimumValue(): mixed
|
||||
{
|
||||
return $this->_platformOptions['min'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum value to enforce on the column.
|
||||
*/
|
||||
public function getMaximumValue(): mixed
|
||||
{
|
||||
return $this->_platformOptions['max'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy.
|
||||
*
|
||||
* Returns the name of the DEFAULT constraint that implements the default value for the column on SQL Server.
|
||||
*
|
||||
* @return ?non-empty-string
|
||||
*/
|
||||
public function getDefaultConstraintName(): ?string
|
||||
{
|
||||
return $this->_platformOptions[SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()} or {@see getMaximumValue()}
|
||||
* instead.
|
||||
*
|
||||
* @return PlatformOptions
|
||||
*/
|
||||
public function getPlatformOptions(): array
|
||||
{
|
||||
return $this->_platformOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()} or {@see getMaximumValue()}
|
||||
* instead.
|
||||
*
|
||||
* @param key-of<PlatformOptions> $name
|
||||
*/
|
||||
public function hasPlatformOption(string $name): bool
|
||||
{
|
||||
return isset($this->_platformOptions[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getCharset()}, {@see getCollation()}, {@see getMinimumValue()} or {@see getMaximumValue()}
|
||||
* instead.
|
||||
*
|
||||
* @param key-of<PlatformOptions> $name
|
||||
*/
|
||||
public function getPlatformOption(string $name): mixed
|
||||
{
|
||||
/** @phpstan-ignore offsetAccess.notFound */
|
||||
return $this->_platformOptions[$name];
|
||||
}
|
||||
|
||||
public function getColumnDefinition(): ?string
|
||||
{
|
||||
return $this->_columnDefinition;
|
||||
}
|
||||
|
||||
public function getAutoincrement(): bool
|
||||
{
|
||||
return $this->_autoincrement;
|
||||
}
|
||||
|
||||
public function setAutoincrement(bool $flag): self
|
||||
{
|
||||
$this->_autoincrement = $flag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): self
|
||||
{
|
||||
$this->_comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->_comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $values
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setValues(array $values): static
|
||||
{
|
||||
$this->_values = $values;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function getValues(): array
|
||||
{
|
||||
return $this->_values;
|
||||
}
|
||||
|
||||
/** @return ColumnProperties */
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge([
|
||||
'name' => $this->_name,
|
||||
'type' => $this->_type,
|
||||
'default' => $this->_default,
|
||||
'notnull' => $this->_notnull,
|
||||
'length' => $this->_length,
|
||||
'precision' => $this->_precision,
|
||||
'scale' => $this->_scale,
|
||||
'fixed' => $this->_fixed,
|
||||
'unsigned' => $this->_unsigned,
|
||||
'autoincrement' => $this->_autoincrement,
|
||||
'columnDefinition' => $this->_columnDefinition,
|
||||
'comment' => $this->_comment,
|
||||
'values' => $this->_values,
|
||||
], $this->_platformOptions);
|
||||
}
|
||||
|
||||
public static function editor(): ColumnEditor
|
||||
{
|
||||
return new ColumnEditor();
|
||||
}
|
||||
|
||||
public function edit(): ColumnEditor
|
||||
{
|
||||
return self::editor()
|
||||
->setName($this->getObjectName())
|
||||
->setType($this->_type)
|
||||
->setLength($this->_length)
|
||||
->setPrecision($this->_precision)
|
||||
->setScale($this->_scale)
|
||||
->setUnsigned($this->_unsigned)
|
||||
->setFixed($this->_fixed)
|
||||
->setNotNull($this->_notnull)
|
||||
->setDefaultValue($this->_default)
|
||||
->setAutoincrement($this->_autoincrement)
|
||||
->setComment($this->_comment)
|
||||
->setValues($this->_values)
|
||||
->setColumnDefinition($this->_columnDefinition)
|
||||
->setCharset($this->getCharset())
|
||||
->setCollation($this->getCollation())
|
||||
->setMinimumValue($this->getMinimumValue())
|
||||
->setMaximumValue($this->getMaximumValue())
|
||||
->setDefaultConstraintName($this->getDefaultConstraintName());
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use function strcasecmp;
|
||||
|
||||
/**
|
||||
* Represents the change of a column.
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ColumnDiff
|
||||
{
|
||||
/** @internal The diff can be only instantiated by a {@see Comparator}. */
|
||||
public function __construct(private readonly Column $oldColumn, private readonly Column $newColumn)
|
||||
{
|
||||
}
|
||||
|
||||
public function countChangedProperties(): int
|
||||
{
|
||||
return (int) $this->hasUnsignedChanged()
|
||||
+ (int) $this->hasAutoIncrementChanged()
|
||||
+ (int) $this->hasDefaultChanged()
|
||||
+ (int) $this->hasFixedChanged()
|
||||
+ (int) $this->hasPrecisionChanged()
|
||||
+ (int) $this->hasScaleChanged()
|
||||
+ (int) $this->hasLengthChanged()
|
||||
+ (int) $this->hasNotNullChanged()
|
||||
+ (int) $this->hasNameChanged()
|
||||
+ (int) $this->hasTypeChanged()
|
||||
+ (int) $this->hasPlatformOptionsChanged()
|
||||
+ (int) $this->hasCommentChanged();
|
||||
}
|
||||
|
||||
public function getOldColumn(): Column
|
||||
{
|
||||
return $this->oldColumn;
|
||||
}
|
||||
|
||||
public function getNewColumn(): Column
|
||||
{
|
||||
return $this->newColumn;
|
||||
}
|
||||
|
||||
public function hasNameChanged(): bool
|
||||
{
|
||||
$oldColumn = $this->getOldColumn();
|
||||
|
||||
// Column names are case insensitive
|
||||
return strcasecmp($oldColumn->getName(), $this->getNewColumn()->getName()) !== 0;
|
||||
}
|
||||
|
||||
public function hasTypeChanged(): bool
|
||||
{
|
||||
return $this->newColumn->getType()::class !== $this->oldColumn->getType()::class;
|
||||
}
|
||||
|
||||
public function hasLengthChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): ?int {
|
||||
return $column->getLength();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasPrecisionChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): ?int {
|
||||
return $column->getPrecision();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasScaleChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): int {
|
||||
return $column->getScale();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasUnsignedChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): bool {
|
||||
return $column->getUnsigned();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasFixedChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): bool {
|
||||
return $column->getFixed();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasNotNullChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): bool {
|
||||
return $column->getNotnull();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasDefaultChanged(): bool
|
||||
{
|
||||
$oldDefault = $this->oldColumn->getDefault();
|
||||
$newDefault = $this->newColumn->getDefault();
|
||||
|
||||
// Null values need to be checked additionally as they tell whether to create or drop a default value.
|
||||
// null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
|
||||
if (($newDefault === null) xor ($oldDefault === null)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $newDefault != $oldDefault;
|
||||
}
|
||||
|
||||
public function hasAutoIncrementChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): bool {
|
||||
return $column->getAutoincrement();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasCommentChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): string {
|
||||
return $column->getComment();
|
||||
});
|
||||
}
|
||||
|
||||
public function hasPlatformOptionsChanged(): bool
|
||||
{
|
||||
return $this->hasPropertyChanged(static function (Column $column): array {
|
||||
return $column->getPlatformOptions();
|
||||
});
|
||||
}
|
||||
|
||||
private function hasPropertyChanged(callable $property): bool
|
||||
{
|
||||
return $property($this->newColumn) !== $property($this->oldColumn);
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidColumnDefinition;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Types\Exception\TypesException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
final class ColumnEditor
|
||||
{
|
||||
private ?UnqualifiedName $name = null;
|
||||
|
||||
private ?Type $type = null;
|
||||
|
||||
private ?int $length = null;
|
||||
|
||||
private ?int $precision = null;
|
||||
|
||||
private int $scale = 0;
|
||||
|
||||
private bool $unsigned = false;
|
||||
|
||||
private bool $fixed = false;
|
||||
|
||||
private bool $notNull = true;
|
||||
|
||||
private mixed $defaultValue = null;
|
||||
|
||||
private mixed $minimumValue = null;
|
||||
|
||||
private mixed $maximumValue = null;
|
||||
|
||||
private bool $autoincrement = false;
|
||||
|
||||
private string $comment = '';
|
||||
|
||||
/** @var list<string> */
|
||||
private array $values = [];
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
private ?string $charset = null;
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
private ?string $collation = null;
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
private ?string $defaultConstraintName = null;
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
private ?string $columnDefinition = null;
|
||||
|
||||
/** @internal Use {@link Column::editor()} or {@link Column::edit()} to create an instance */
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function setName(UnqualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setUnquotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::unquoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setQuotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::quoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(Type $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @throws TypesException */
|
||||
public function setTypeName(string $typeName): self
|
||||
{
|
||||
$this->type = Type::getType($typeName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLength(?int $length): self
|
||||
{
|
||||
$this->length = $length;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPrecision(?int $precision): self
|
||||
{
|
||||
$this->precision = $precision;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setScale(int $scale): self
|
||||
{
|
||||
$this->scale = $scale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUnsigned(bool $unsigned): self
|
||||
{
|
||||
$this->unsigned = $unsigned;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFixed(bool $fixed): self
|
||||
{
|
||||
$this->fixed = $fixed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setNotNull(bool $notNull): self
|
||||
{
|
||||
$this->notNull = $notNull;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDefaultValue(mixed $defaultValue): self
|
||||
{
|
||||
$this->defaultValue = $defaultValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMinimumValue(mixed $minimumValue): self
|
||||
{
|
||||
$this->minimumValue = $minimumValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaximumValue(mixed $maximumValue): self
|
||||
{
|
||||
$this->maximumValue = $maximumValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAutoincrement(bool $flag): self
|
||||
{
|
||||
$this->autoincrement = $flag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param list<string> $values */
|
||||
public function setValues(array $values): self
|
||||
{
|
||||
$this->values = $values;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param ?non-empty-string $charset */
|
||||
public function setCharset(?string $charset): self
|
||||
{
|
||||
$this->charset = $charset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param ?non-empty-string $collation */
|
||||
public function setCollation(?string $collation): self
|
||||
{
|
||||
$this->collation = $collation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Should be used only from within the {@see AbstractSchemaManager} class hierarchy.
|
||||
*
|
||||
* @param ?non-empty-string $defaultConstraintName
|
||||
*/
|
||||
public function setDefaultConstraintName(?string $defaultConstraintName): self
|
||||
{
|
||||
$this->defaultConstraintName = $defaultConstraintName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param ?non-empty-string $columnDefinition */
|
||||
public function setColumnDefinition(?string $columnDefinition): self
|
||||
{
|
||||
$this->columnDefinition = $columnDefinition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): Column
|
||||
{
|
||||
if ($this->name === null) {
|
||||
throw InvalidColumnDefinition::nameNotSpecified();
|
||||
}
|
||||
|
||||
if ($this->type === null) {
|
||||
throw InvalidColumnDefinition::dataTypeNotSpecified($this->name);
|
||||
}
|
||||
|
||||
$platformOptions = [];
|
||||
|
||||
if ($this->charset !== null) {
|
||||
$platformOptions['charset'] = $this->charset;
|
||||
}
|
||||
|
||||
if ($this->collation !== null) {
|
||||
$platformOptions['collation'] = $this->collation;
|
||||
}
|
||||
|
||||
if ($this->minimumValue !== null) {
|
||||
$platformOptions['min'] = $this->minimumValue;
|
||||
}
|
||||
|
||||
if ($this->maximumValue !== null) {
|
||||
$platformOptions['max'] = $this->maximumValue;
|
||||
}
|
||||
|
||||
if ($this->defaultConstraintName !== null) {
|
||||
$platformOptions[SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME] = $this->defaultConstraintName;
|
||||
}
|
||||
|
||||
return new Column(
|
||||
$this->name->toString(),
|
||||
$this->type,
|
||||
[
|
||||
'length' => $this->length,
|
||||
'precision' => $this->precision,
|
||||
'scale' => $this->scale,
|
||||
'unsigned' => $this->unsigned,
|
||||
'fixed' => $this->fixed,
|
||||
'notnull' => $this->notNull,
|
||||
'default' => $this->defaultValue,
|
||||
'autoincrement' => $this->autoincrement,
|
||||
'comment' => $this->comment,
|
||||
'values' => $this->values,
|
||||
'platformOptions' => $platformOptions,
|
||||
'columnDefinition' => $this->columnDefinition,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Compares two Schemas and return an instance of SchemaDiff.
|
||||
*/
|
||||
class Comparator
|
||||
{
|
||||
/** @internal The comparator can be only instantiated by a schema manager. */
|
||||
public function __construct(
|
||||
private readonly AbstractPlatform $platform,
|
||||
private readonly ComparatorConfig $config = new ComparatorConfig(),
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the differences between the schemas.
|
||||
*/
|
||||
public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff
|
||||
{
|
||||
$createdSchemas = [];
|
||||
$droppedSchemas = [];
|
||||
$createdTables = [];
|
||||
$alteredTables = [];
|
||||
$droppedTables = [];
|
||||
$createdSequences = [];
|
||||
$alteredSequences = [];
|
||||
$droppedSequences = [];
|
||||
|
||||
foreach ($newSchema->getNamespaces() as $newNamespace) {
|
||||
if ($oldSchema->hasNamespace($newNamespace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$createdSchemas[] = $newNamespace;
|
||||
}
|
||||
|
||||
foreach ($oldSchema->getNamespaces() as $oldNamespace) {
|
||||
if ($newSchema->hasNamespace($oldNamespace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$droppedSchemas[] = $oldNamespace;
|
||||
}
|
||||
|
||||
foreach ($newSchema->getTables() as $newTable) {
|
||||
$newTableName = $newTable->getShortestName($newSchema->getName());
|
||||
if (! $oldSchema->hasTable($newTableName)) {
|
||||
$createdTables[] = $newSchema->getTable($newTableName);
|
||||
} else {
|
||||
$tableDiff = $this->compareTables(
|
||||
$oldSchema->getTable($newTableName),
|
||||
$newSchema->getTable($newTableName),
|
||||
);
|
||||
|
||||
if (! $tableDiff->isEmpty()) {
|
||||
$alteredTables[] = $tableDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are tables removed
|
||||
foreach ($oldSchema->getTables() as $oldTable) {
|
||||
$oldTableName = $oldTable->getShortestName($oldSchema->getName());
|
||||
|
||||
$oldTable = $oldSchema->getTable($oldTableName);
|
||||
if ($newSchema->hasTable($oldTableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$droppedTables[] = $oldTable;
|
||||
}
|
||||
|
||||
foreach ($newSchema->getSequences() as $newSequence) {
|
||||
$newSequenceName = $newSequence->getShortestName($newSchema->getName());
|
||||
if (! $oldSchema->hasSequence($newSequenceName)) {
|
||||
if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) {
|
||||
$createdSequences[] = $newSequence;
|
||||
}
|
||||
} else {
|
||||
if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) {
|
||||
$alteredSequences[] = $newSchema->getSequence($newSequenceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($oldSchema->getSequences() as $oldSequence) {
|
||||
if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldSequenceName = $oldSequence->getShortestName($oldSchema->getName());
|
||||
|
||||
if ($newSchema->hasSequence($oldSequenceName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$droppedSequences[] = $oldSequence;
|
||||
}
|
||||
|
||||
return new SchemaDiff(
|
||||
$createdSchemas,
|
||||
$droppedSchemas,
|
||||
$createdTables,
|
||||
$alteredTables,
|
||||
$droppedTables,
|
||||
$createdSequences,
|
||||
$alteredSequences,
|
||||
$droppedSequences,
|
||||
);
|
||||
}
|
||||
|
||||
private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool
|
||||
{
|
||||
foreach ($schema->getTables() as $table) {
|
||||
if ($sequence->isAutoIncrementsFor($table)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool
|
||||
{
|
||||
if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the tables and returns the difference between them.
|
||||
*/
|
||||
public function compareTables(Table $oldTable, Table $newTable): TableDiff
|
||||
{
|
||||
$shouldReportModifiedIndexes = $this->config->getReportModifiedIndexes();
|
||||
if ($shouldReportModifiedIndexes) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6890',
|
||||
'Detection of modified indexes is deprecated. Please disable it by configuring the comparator'
|
||||
. ' using ComparatorConfig::withReportModifiedIndexes(false).',
|
||||
);
|
||||
}
|
||||
|
||||
$addedColumns = [];
|
||||
$modifiedColumns = [];
|
||||
$droppedColumns = [];
|
||||
$addedIndexes = [];
|
||||
$modifiedIndexes = [];
|
||||
$droppedIndexes = [];
|
||||
$renamedIndexes = [];
|
||||
$addedForeignKeys = [];
|
||||
$droppedForeignKeys = [];
|
||||
|
||||
$oldColumns = $oldTable->getColumns();
|
||||
$newColumns = $newTable->getColumns();
|
||||
|
||||
// See if all the columns in the old table exist in the new table
|
||||
foreach ($newColumns as $newColumn) {
|
||||
$newColumnName = strtolower($newColumn->getName());
|
||||
|
||||
if ($oldTable->hasColumn($newColumnName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$addedColumns[$newColumnName] = $newColumn;
|
||||
}
|
||||
|
||||
// See if there are any removed columns in the new table
|
||||
foreach ($oldColumns as $oldColumn) {
|
||||
$oldColumnName = strtolower($oldColumn->getName());
|
||||
|
||||
// See if column is removed in the new table.
|
||||
if (! $newTable->hasColumn($oldColumnName)) {
|
||||
$droppedColumns[$oldColumnName] = $oldColumn;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$newColumn = $newTable->getColumn($oldColumnName);
|
||||
|
||||
if ($this->columnsEqual($oldColumn, $newColumn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modifiedColumns[$oldColumnName] = new ColumnDiff($oldColumn, $newColumn);
|
||||
}
|
||||
|
||||
$renamedColumnNames = $newTable->getRenamedColumns();
|
||||
|
||||
foreach ($addedColumns as $addedColumnName => $addedColumn) {
|
||||
if (! isset($renamedColumnNames[$addedColumn->getName()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$removedColumnName = strtolower($renamedColumnNames[$addedColumn->getName()]);
|
||||
// Explicitly renamed columns need to be diffed, because their types can also have changed
|
||||
$modifiedColumns[$removedColumnName] = new ColumnDiff(
|
||||
$droppedColumns[$removedColumnName],
|
||||
$addedColumn,
|
||||
);
|
||||
|
||||
unset(
|
||||
$addedColumns[$addedColumnName],
|
||||
$droppedColumns[$removedColumnName],
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->config->getDetectRenamedColumns()) {
|
||||
$this->detectRenamedColumns($modifiedColumns, $addedColumns, $droppedColumns);
|
||||
}
|
||||
|
||||
$oldIndexes = $oldTable->getIndexes();
|
||||
$newIndexes = $newTable->getIndexes();
|
||||
|
||||
// See if all the indexes from the old table exist in the new one
|
||||
foreach ($newIndexes as $newIndexName => $newIndex) {
|
||||
if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$addedIndexes[$newIndexName] = $newIndex;
|
||||
}
|
||||
|
||||
// See if there are any removed indexes in the new table
|
||||
foreach ($oldIndexes as $oldIndexName => $oldIndex) {
|
||||
// See if the index is removed in the new table.
|
||||
if (
|
||||
($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) ||
|
||||
! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName)
|
||||
) {
|
||||
$droppedIndexes[$oldIndexName] = $oldIndex;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// See if index has changed in the new table.
|
||||
$newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName);
|
||||
assert($newIndex instanceof Index);
|
||||
|
||||
if (! $this->diffIndex($oldIndex, $newIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($shouldReportModifiedIndexes) {
|
||||
$modifiedIndexes[] = $newIndex;
|
||||
} else {
|
||||
$droppedIndexes[$oldIndexName] = $oldIndex;
|
||||
$addedIndexes[$oldIndexName] = $newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->config->getDetectRenamedIndexes()) {
|
||||
$renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes);
|
||||
}
|
||||
|
||||
$oldForeignKeys = $oldTable->getForeignKeys();
|
||||
$newForeignKeys = $newTable->getForeignKeys();
|
||||
|
||||
foreach ($oldForeignKeys as $oldKey => $oldForeignKey) {
|
||||
foreach ($newForeignKeys as $newKey => $newForeignKey) {
|
||||
if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) {
|
||||
unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
|
||||
} else {
|
||||
if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) {
|
||||
$droppedForeignKeys[$oldKey] = $oldForeignKey;
|
||||
$addedForeignKeys[$newKey] = $newForeignKey;
|
||||
|
||||
unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($oldForeignKeys as $oldForeignKey) {
|
||||
$droppedForeignKeys[] = $oldForeignKey;
|
||||
}
|
||||
|
||||
foreach ($newForeignKeys as $newForeignKey) {
|
||||
$addedForeignKeys[] = $newForeignKey;
|
||||
}
|
||||
|
||||
return new TableDiff(
|
||||
$oldTable,
|
||||
addedColumns: $addedColumns,
|
||||
changedColumns: $modifiedColumns,
|
||||
droppedColumns: $droppedColumns,
|
||||
addedIndexes: $addedIndexes,
|
||||
modifiedIndexes: $modifiedIndexes,
|
||||
droppedIndexes: $droppedIndexes,
|
||||
renamedIndexes: $renamedIndexes,
|
||||
addedForeignKeys: $addedForeignKeys,
|
||||
droppedForeignKeys: $droppedForeignKeys,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
|
||||
* however ambiguities between different possibilities should not lead to renaming at all.
|
||||
*
|
||||
* @param array<string,ColumnDiff> $modifiedColumns
|
||||
* @param array<string,Column> $addedColumns
|
||||
* @param array<string,Column> $removedColumns
|
||||
*/
|
||||
private function detectRenamedColumns(array &$modifiedColumns, array &$addedColumns, array &$removedColumns): void
|
||||
{
|
||||
/** @var array<string, array<array<Column>>> $candidatesByName */
|
||||
$candidatesByName = [];
|
||||
|
||||
foreach ($addedColumns as $addedColumnName => $addedColumn) {
|
||||
foreach ($removedColumns as $removedColumn) {
|
||||
if (! $this->columnsEqual($addedColumn, $removedColumn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidatesByName[$addedColumnName][] = [$removedColumn, $addedColumn];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidatesByName as $addedColumnName => $candidates) {
|
||||
if (count($candidates) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$oldColumn, $newColumn] = $candidates[0];
|
||||
$oldColumnName = strtolower($oldColumn->getName());
|
||||
|
||||
if (isset($modifiedColumns[$oldColumnName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modifiedColumns[$oldColumnName] = new ColumnDiff(
|
||||
$oldColumn,
|
||||
$newColumn,
|
||||
);
|
||||
|
||||
unset(
|
||||
$addedColumns[$addedColumnName],
|
||||
$removedColumns[$oldColumnName],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
|
||||
* however ambiguities between different possibilities should not lead to renaming at all.
|
||||
*
|
||||
* @param array<string,Index> $addedIndexes
|
||||
* @param array<string,Index> $removedIndexes
|
||||
*
|
||||
* @return array<string,Index>
|
||||
*/
|
||||
private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array
|
||||
{
|
||||
$candidatesByName = [];
|
||||
|
||||
// Gather possible rename candidates by comparing each added and removed index based on semantics.
|
||||
foreach ($addedIndexes as $addedIndexName => $addedIndex) {
|
||||
foreach ($removedIndexes as $removedIndex) {
|
||||
if ($this->diffIndex($addedIndex, $removedIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
|
||||
}
|
||||
}
|
||||
|
||||
$renamedIndexes = [];
|
||||
|
||||
foreach ($candidatesByName as $candidates) {
|
||||
// If the current rename candidate contains exactly one semantically equal index,
|
||||
// we can safely rename it.
|
||||
// Otherwise, it is unclear if a rename action is really intended,
|
||||
// therefore we let those ambiguous indexes be added/dropped.
|
||||
if (count($candidates) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$removedIndex, $addedIndex] = $candidates[0];
|
||||
|
||||
$removedIndexName = strtolower($removedIndex->getName());
|
||||
$addedIndexName = strtolower($addedIndex->getName());
|
||||
|
||||
if (isset($renamedIndexes[$removedIndexName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$renamedIndexes[$removedIndexName] = $addedIndex;
|
||||
unset(
|
||||
$addedIndexes[$addedIndexName],
|
||||
$removedIndexes[$removedIndexName],
|
||||
);
|
||||
}
|
||||
|
||||
return $renamedIndexes;
|
||||
}
|
||||
|
||||
protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool
|
||||
{
|
||||
if (
|
||||
array_map('strtolower', $key1->getUnquotedLocalColumns())
|
||||
!== array_map('strtolower', $key2->getUnquotedLocalColumns())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
array_map('strtolower', $key1->getUnquotedForeignColumns())
|
||||
!== array_map('strtolower', $key2->getUnquotedForeignColumns())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($key1->onUpdate() !== $key2->onUpdate()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $key1->onDelete() !== $key2->onDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the definitions of the given columns
|
||||
*/
|
||||
protected function columnsEqual(Column $column1, Column $column2): bool
|
||||
{
|
||||
return $this->platform->columnsEqual($column1, $column2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the difference between the indexes $index1 and $index2.
|
||||
*
|
||||
* Compares $index1 with $index2 and returns true if there are any
|
||||
* differences or false in case there are no differences.
|
||||
*/
|
||||
protected function diffIndex(Index $index1, Index $index2): bool
|
||||
{
|
||||
return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
final class ComparatorConfig
|
||||
{
|
||||
public function __construct(
|
||||
private readonly bool $detectRenamedColumns = true,
|
||||
private readonly bool $detectRenamedIndexes = true,
|
||||
private readonly bool $reportModifiedIndexes = true,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withDetectRenamedColumns(bool $detectRenamedColumns): self
|
||||
{
|
||||
return new self(
|
||||
$detectRenamedColumns,
|
||||
$this->detectRenamedIndexes,
|
||||
$this->reportModifiedIndexes,
|
||||
);
|
||||
}
|
||||
|
||||
public function getDetectRenamedColumns(): bool
|
||||
{
|
||||
return $this->detectRenamedColumns;
|
||||
}
|
||||
|
||||
public function withDetectRenamedIndexes(bool $detectRenamedIndexes): self
|
||||
{
|
||||
return new self(
|
||||
$this->detectRenamedColumns,
|
||||
$detectRenamedIndexes,
|
||||
$this->reportModifiedIndexes,
|
||||
);
|
||||
}
|
||||
|
||||
public function getDetectRenamedIndexes(): bool
|
||||
{
|
||||
return $this->detectRenamedIndexes;
|
||||
}
|
||||
|
||||
public function withReportModifiedIndexes(bool $reportModifiedIndexes): self
|
||||
{
|
||||
return new self(
|
||||
$this->detectRenamedColumns,
|
||||
$this->detectRenamedIndexes,
|
||||
$reportModifiedIndexes,
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal This method is intended solely to provide an upgrade path to DBAL 5.0. */
|
||||
public function getReportModifiedIndexes(): bool
|
||||
{
|
||||
return $this->reportModifiedIndexes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\DB2Platform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function implode;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* IBM Db2 Schema Manager.
|
||||
*
|
||||
* @link https://www.ibm.com/docs/en/db2/11.5?topic=sql-catalog-views
|
||||
*
|
||||
* @extends AbstractSchemaManager<DB2Platform>
|
||||
*/
|
||||
class DB2SchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
|
||||
{
|
||||
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
|
||||
|
||||
$length = $precision = $default = null;
|
||||
$scale = 0;
|
||||
$fixed = false;
|
||||
|
||||
if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') {
|
||||
$default = $tableColumn['default'];
|
||||
|
||||
if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
|
||||
$default = str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($tableColumn['typename']);
|
||||
|
||||
switch (strtolower($tableColumn['typename'])) {
|
||||
case 'varchar':
|
||||
if ($tableColumn['codepage'] === 0) {
|
||||
$type = Types::BINARY;
|
||||
}
|
||||
|
||||
$length = $tableColumn['length'];
|
||||
break;
|
||||
|
||||
case 'character':
|
||||
if ($tableColumn['codepage'] === 0) {
|
||||
$type = Types::BINARY;
|
||||
}
|
||||
|
||||
$length = $tableColumn['length'];
|
||||
$fixed = true;
|
||||
break;
|
||||
|
||||
case 'clob':
|
||||
$length = $tableColumn['length'];
|
||||
break;
|
||||
|
||||
case 'decimal':
|
||||
case 'double':
|
||||
case 'real':
|
||||
$scale = $tableColumn['scale'];
|
||||
$precision = $tableColumn['length'];
|
||||
break;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'length' => $length,
|
||||
'fixed' => $fixed,
|
||||
'default' => $default,
|
||||
'autoincrement' => (bool) $tableColumn['autoincrement'],
|
||||
'notnull' => $tableColumn['nulls'] === 'N',
|
||||
];
|
||||
|
||||
if ($tableColumn['comment'] !== null) {
|
||||
$options['comment'] = $tableColumn['comment'];
|
||||
}
|
||||
|
||||
if ($scale !== null && $precision !== null) {
|
||||
$options['scale'] = $scale;
|
||||
$options['precision'] = $precision;
|
||||
}
|
||||
|
||||
return new Column($tableColumn['colname'], Type::getType($type), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
$table = array_change_key_case($table, CASE_LOWER);
|
||||
|
||||
return $table['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableIndexesList(array $rows, string $tableName): array
|
||||
{
|
||||
foreach ($rows as &$row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
$row['primary'] = (bool) $row['primary'];
|
||||
}
|
||||
|
||||
return parent::_getPortableTableIndexesList($rows, $tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
return new ForeignKeyConstraint(
|
||||
$tableForeignKey['local_columns'],
|
||||
$tableForeignKey['foreign_table'],
|
||||
$tableForeignKey['foreign_columns'],
|
||||
$tableForeignKey['name'],
|
||||
$tableForeignKey['options'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeysList(array $rows): array
|
||||
{
|
||||
$foreignKeys = [];
|
||||
|
||||
foreach ($rows as $tableForeignKey) {
|
||||
$tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER);
|
||||
|
||||
if (! isset($foreignKeys[$tableForeignKey['index_name']])) {
|
||||
$foreignKeys[$tableForeignKey['index_name']] = [
|
||||
'local_columns' => [$tableForeignKey['local_column']],
|
||||
'foreign_table' => $tableForeignKey['foreign_table'],
|
||||
'foreign_columns' => [$tableForeignKey['foreign_column']],
|
||||
'name' => $tableForeignKey['index_name'],
|
||||
'options' => [
|
||||
'onUpdate' => $tableForeignKey['on_update'],
|
||||
'onDelete' => $tableForeignKey['on_delete'],
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$foreignKeys[$tableForeignKey['index_name']]['local_columns'][] = $tableForeignKey['local_column'];
|
||||
$foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column'];
|
||||
}
|
||||
}
|
||||
|
||||
return parent::_getPortableTableForeignKeysList($foreignKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
$view = array_change_key_case($view, CASE_LOWER);
|
||||
|
||||
$sql = '';
|
||||
$pos = strpos($view['text'], ' AS ');
|
||||
|
||||
if ($pos !== false) {
|
||||
$sql = substr($view['text'], $pos + 4);
|
||||
}
|
||||
|
||||
return new View($view['name'], $sql);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see Identifier::toNormalizedValue()} instead. */
|
||||
protected function normalizeName(string $name): string
|
||||
{
|
||||
$identifier = new Identifier($name);
|
||||
|
||||
return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name);
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT TABNAME AS NAME
|
||||
FROM SYSCAT.TABLES
|
||||
WHERE TYPE = 'T'
|
||||
AND TABSCHEMA = ?
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql, [$databaseName]);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['C.TABSCHEMA = ?'];
|
||||
$params = [$databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'C.TABNAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
C.TABNAME AS NAME,
|
||||
C.COLNAME,
|
||||
C.TYPENAME,
|
||||
C.CODEPAGE,
|
||||
C.NULLS,
|
||||
C.LENGTH,
|
||||
C.SCALE,
|
||||
C.REMARKS AS COMMENT,
|
||||
CASE
|
||||
WHEN C.GENERATED = 'D' THEN 1
|
||||
ELSE 0
|
||||
END AS AUTOINCREMENT,
|
||||
C.DEFAULT
|
||||
FROM SYSCAT.COLUMNS C
|
||||
JOIN SYSCAT.TABLES AS T
|
||||
ON T.TABSCHEMA = C.TABSCHEMA
|
||||
AND T.TABNAME = C.TABNAME
|
||||
WHERE %s
|
||||
AND T.TYPE = 'T'
|
||||
ORDER BY C.TABNAME, C.COLNO
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['IDX.TABSCHEMA = ?'];
|
||||
$params = [$databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'IDX.TABNAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
IDX.TABNAME AS NAME,
|
||||
IDX.INDNAME AS KEY_NAME,
|
||||
IDXCOL.COLNAME AS COLUMN_NAME,
|
||||
CASE
|
||||
WHEN IDX.UNIQUERULE = 'P' THEN 1
|
||||
ELSE 0
|
||||
END AS PRIMARY,
|
||||
CASE
|
||||
WHEN IDX.UNIQUERULE = 'D' THEN 1
|
||||
ELSE 0
|
||||
END AS NON_UNIQUE
|
||||
FROM SYSCAT.INDEXES AS IDX
|
||||
JOIN SYSCAT.TABLES AS T
|
||||
ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME
|
||||
JOIN SYSCAT.INDEXCOLUSE AS IDXCOL
|
||||
ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME
|
||||
WHERE %s
|
||||
AND T.TYPE = 'T'
|
||||
ORDER BY IDX.TABNAME,
|
||||
IDX.INDNAME,
|
||||
IDXCOL.COLSEQ
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['R.TABSCHEMA = ?'];
|
||||
$params = [$databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'R.TABNAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
R.TABNAME AS NAME,
|
||||
FKCOL.COLNAME AS LOCAL_COLUMN,
|
||||
R.REFTABNAME AS FOREIGN_TABLE,
|
||||
PKCOL.COLNAME AS FOREIGN_COLUMN,
|
||||
R.CONSTNAME AS INDEX_NAME,
|
||||
CASE
|
||||
WHEN R.UPDATERULE = 'R' THEN 'RESTRICT'
|
||||
END AS ON_UPDATE,
|
||||
CASE
|
||||
WHEN R.DELETERULE = 'C' THEN 'CASCADE'
|
||||
WHEN R.DELETERULE = 'N' THEN 'SET NULL'
|
||||
WHEN R.DELETERULE = 'R' THEN 'RESTRICT'
|
||||
END AS ON_DELETE
|
||||
FROM SYSCAT.REFERENCES AS R
|
||||
JOIN SYSCAT.TABLES AS T
|
||||
ON T.TABSCHEMA = R.TABSCHEMA
|
||||
AND T.TABNAME = R.TABNAME
|
||||
JOIN SYSCAT.KEYCOLUSE AS FKCOL
|
||||
ON FKCOL.CONSTNAME = R.CONSTNAME
|
||||
AND FKCOL.TABSCHEMA = R.TABSCHEMA
|
||||
AND FKCOL.TABNAME = R.TABNAME
|
||||
JOIN SYSCAT.KEYCOLUSE AS PKCOL
|
||||
ON PKCOL.CONSTNAME = R.REFKEYNAME
|
||||
AND PKCOL.TABSCHEMA = R.REFTABSCHEMA
|
||||
AND PKCOL.TABNAME = R.REFTABNAME
|
||||
AND PKCOL.COLSEQ = FKCOL.COLSEQ
|
||||
WHERE %s
|
||||
AND T.TYPE = 'T'
|
||||
ORDER BY R.TABNAME,
|
||||
R.CONSTNAME,
|
||||
FKCOL.COLSEQ
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$conditions = ['TABSCHEMA = ?'];
|
||||
$params = [$databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'TABNAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT TABNAME,
|
||||
REMARKS
|
||||
FROM SYSCAT.TABLES
|
||||
WHERE %s
|
||||
AND TYPE = 'T'
|
||||
ORDER BY TABNAME
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($this->connection->iterateKeyValue($sql, $params) as $table => $remarks) {
|
||||
$tableOptions[$table] = ['comment' => $remarks];
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception;
|
||||
|
||||
/**
|
||||
* A schema manager factory that returns the default schema manager for the given platform.
|
||||
*/
|
||||
final class DefaultSchemaManagerFactory implements SchemaManagerFactory
|
||||
{
|
||||
/** @throws Exception If the platform does not support creating schema managers yet. */
|
||||
public function createSchemaManager(Connection $connection): AbstractSchemaManager
|
||||
{
|
||||
return $connection->getDatabasePlatform()->createSchemaManager($connection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ColumnAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $tableName, string $columnName): self
|
||||
{
|
||||
return new self(sprintf('The column "%s" on table "%s" already exists.', $columnName, $tableName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ColumnDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $columnName, string $table): self
|
||||
{
|
||||
return new self(sprintf('There is no column with name "%s" on table "%s".', $columnName, $table));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ForeignKeyDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $foreignKeyName, string $table): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('There exists no foreign key with the name "%s" on table "%s".', $foreignKeyName, $table),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class IndexAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $indexName, string $table): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('An index with name "%s" was already defined on table "%s".', $indexName, $table),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class IndexDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $indexName, string $table): self
|
||||
{
|
||||
return new self(sprintf('Index "%s" does not exist on table "%s".', $indexName, $table));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class IndexNameInvalid extends InvalidArgumentException implements SchemaException
|
||||
{
|
||||
public static function new(string $indexName): self
|
||||
{
|
||||
return new self(sprintf('Invalid index name "%s" given, has to be [a-zA-Z0-9_].', $indexName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @psalm-immutable */
|
||||
final class InvalidColumnDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function nameNotSpecified(): self
|
||||
{
|
||||
return new self('Column name is not specified.');
|
||||
}
|
||||
|
||||
public static function dataTypeNotSpecified(UnqualifiedName $columnName): self
|
||||
{
|
||||
return new self(sprintf('Data type is not specified for column %s.', $columnName->toString()));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidForeignKeyConstraintDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function referencedTableNameNotSet(?UnqualifiedName $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Referenced table name is not set for foreign key constraint %s.',
|
||||
self::formatName($constraintName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function referencingColumnNamesNotSet(?UnqualifiedName $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Referencing column names are not set for foreign key constraint %s.',
|
||||
self::formatName($constraintName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function referencedColumnNamesNotSet(?UnqualifiedName $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Referenced column names are not set for foreign key constraint %s.',
|
||||
self::formatName($constraintName),
|
||||
));
|
||||
}
|
||||
|
||||
private static function formatName(?UnqualifiedName $constraintName): string
|
||||
{
|
||||
return $constraintName === null ? '<unnamed>' : $constraintName->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidIdentifier extends InvalidArgumentException implements SchemaException
|
||||
{
|
||||
public static function fromEmpty(): self
|
||||
{
|
||||
return new self('Identifier cannot be empty.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidIndexDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function nameNotSet(): self
|
||||
{
|
||||
return new self('Index name is not set.');
|
||||
}
|
||||
|
||||
public static function columnsNotSet(UnqualifiedName $indexName): self
|
||||
{
|
||||
return new self(sprintf('Columns are not set for index %s.', $indexName->toString()));
|
||||
}
|
||||
|
||||
public static function fromNonPositiveColumnLength(UnqualifiedName $columnName, int $length): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Indexed column length must be a positive integer, %d given for column %s.',
|
||||
$length,
|
||||
$columnName->toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class InvalidName extends InvalidArgumentException implements SchemaException
|
||||
{
|
||||
public static function fromEmpty(): self
|
||||
{
|
||||
return new self('Name cannot be empty.');
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
final class InvalidPrimaryKeyConstraintDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function columnNamesNotSet(): self
|
||||
{
|
||||
return new self('Primary key constraint column names are not set.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidState extends LogicException implements SchemaException
|
||||
{
|
||||
public static function objectNameNotInitialized(): self
|
||||
{
|
||||
return new self('Object name has not been initialized.');
|
||||
}
|
||||
|
||||
public static function indexHasInvalidType(string $indexName): self
|
||||
{
|
||||
return new self(sprintf('Index "%s" has invalid type.', $indexName));
|
||||
}
|
||||
|
||||
public static function indexHasInvalidPredicate(string $indexName): self
|
||||
{
|
||||
return new self(sprintf('Index "%s" has invalid predicate.', $indexName));
|
||||
}
|
||||
|
||||
public static function indexHasInvalidColumns(string $indexName): self
|
||||
{
|
||||
return new self(sprintf('Index "%s" has invalid columns.', $indexName));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidReferencedTableName(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Foreign key constraint "%s" has invalid referenced table name.',
|
||||
$constraintName,
|
||||
));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidReferencingColumnNames(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Foreign key constraint "%s" has one or more invalid referencing column names.',
|
||||
$constraintName,
|
||||
));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidReferencedColumnNames(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Foreign key constraint "%s" has one or more invalid referenced column name.',
|
||||
$constraintName,
|
||||
));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidMatchType(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Foreign key constraint "%s" has invalid match type.', $constraintName));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidOnUpdateAction(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Foreign key constraint "%s" has invalid ON UPDATE action.', $constraintName));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidOnDeleteAction(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Foreign key constraint "%s" has invalid ON DELETE action.', $constraintName));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintHasInvalidDeferrability(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Foreign key constraint "%s" has invalid deferrability.', $constraintName));
|
||||
}
|
||||
|
||||
public static function uniqueConstraintHasInvalidColumnNames(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Unique constraint "%s" has one or more invalid column names.', $constraintName));
|
||||
}
|
||||
|
||||
public static function uniqueConstraintHasEmptyColumnNames(string $constraintName): self
|
||||
{
|
||||
return new self(sprintf('Unique constraint "%s" has no column names.', $constraintName));
|
||||
}
|
||||
|
||||
public static function tableHasInvalidPrimaryKeyConstraint(string $tableName): self
|
||||
{
|
||||
return new self(sprintf('Table "%s" has invalid primary key constraint.', $tableName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidTableDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function nameNotSet(): self
|
||||
{
|
||||
return new self('Table name is not set.');
|
||||
}
|
||||
|
||||
public static function columnsNotSet(OptionallyQualifiedName $tableName): self
|
||||
{
|
||||
return new self(sprintf('Columns are not set for table %s.', $tableName->toString()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
final class InvalidTableModification extends LogicException implements SchemaException
|
||||
{
|
||||
public static function columnAlreadyExists(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectAlreadyExists $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Column %s already exists on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function columnDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectDoesNotExist $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Column %s does not exist on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function indexAlreadyExists(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectAlreadyExists $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Index %s already exists on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function indexDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectDoesNotExist $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Index %s does not exist on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function primaryKeyConstraintAlreadyExists(?OptionallyQualifiedName $tableName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Primary key constraint already exists on table %s.',
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function primaryKeyConstraintDoesNotExist(?OptionallyQualifiedName $tableName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Primary key constraint does not exist on table %s.',
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function uniqueConstraintAlreadyExists(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectAlreadyExists $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Unique constraint %s already exists on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function uniqueConstraintDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectDoesNotExist $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Unique constraint %s does not exist on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintAlreadyExists(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectAlreadyExists $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Foreign key constraint %s already exists on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
ObjectDoesNotExist $previous,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Foreign key constraint %s does not exist on table %s.',
|
||||
$previous->getObjectName()->toString(),
|
||||
self::formatTableName($tableName),
|
||||
), previous: $previous);
|
||||
}
|
||||
|
||||
public static function indexedColumnDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
UnqualifiedName $indexName,
|
||||
UnqualifiedName $columnName,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Column %s referenced by index %s does not exist on table %s.',
|
||||
$columnName->toString(),
|
||||
$indexName->toString(),
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function primaryKeyConstraintColumnDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
?UnqualifiedName $constraintName,
|
||||
UnqualifiedName $columnName,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Column %s referenced by primary key constraint %s does not exist on table %s.',
|
||||
$columnName->toString(),
|
||||
self::formatConstraintName($constraintName),
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function uniqueConstraintColumnDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
?UnqualifiedName $constraintName,
|
||||
UnqualifiedName $columnName,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Column %s referenced by unique constraint %s does not exist on table %s.',
|
||||
$columnName->toString(),
|
||||
self::formatConstraintName($constraintName),
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
public static function foreignKeyConstraintReferencingColumnDoesNotExist(
|
||||
?OptionallyQualifiedName $tableName,
|
||||
?UnqualifiedName $constraintName,
|
||||
UnqualifiedName $columnName,
|
||||
): self {
|
||||
return new self(sprintf(
|
||||
'Referencing column %s of foreign key constraint %s does not exist on table %s.',
|
||||
$columnName->toString(),
|
||||
self::formatConstraintName($constraintName),
|
||||
self::formatTableName($tableName),
|
||||
));
|
||||
}
|
||||
|
||||
private static function formatTableName(?OptionallyQualifiedName $tableName): string
|
||||
{
|
||||
return $tableName === null ? '<unnamed>' : $tableName->toString();
|
||||
}
|
||||
|
||||
private static function formatConstraintName(?UnqualifiedName $constraintName): string
|
||||
{
|
||||
return $constraintName === null ? '<unnamed>' : $constraintName->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidTableName extends InvalidArgumentException implements SchemaException
|
||||
{
|
||||
public static function new(string $tableName): self
|
||||
{
|
||||
return new self(sprintf('Invalid table name specified "%s".', $tableName));
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidUniqueConstraintDefinition extends LogicException implements SchemaException
|
||||
{
|
||||
public static function columnNamesAreNotSet(?UnqualifiedName $constraintName): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Column names are not set for unique constraint %s.',
|
||||
$constraintName === null ? '<unnamed>' : $constraintName->toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class NamespaceAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $namespaceName): self
|
||||
{
|
||||
return new self(sprintf('The namespace with name "%s" already exists.', $namespaceName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class NotImplemented extends LogicException implements SchemaException
|
||||
{
|
||||
public static function fromMethod(string $class, string $method): self
|
||||
{
|
||||
return new self(sprintf('Class %s does not implement method %s().', $class, $method));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PrimaryKeyAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $tableName): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Primary key was already defined on table "%s".', $tableName),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SequenceAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $sequenceName): self
|
||||
{
|
||||
return new self(sprintf('The sequence "%s" already exists.', $sequenceName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SequenceDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $sequenceName): self
|
||||
{
|
||||
return new self(sprintf('There exists no sequence with the name "%s".', $sequenceName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class TableAlreadyExists extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $tableName): self
|
||||
{
|
||||
return new self(sprintf('The table with name "%s" already exists.', $tableName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class TableDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $tableName): self
|
||||
{
|
||||
return new self(sprintf('There is no table with name "%s" in the schema.', $tableName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UniqueConstraintDoesNotExist extends LogicException implements SchemaException
|
||||
{
|
||||
public static function new(string $constraintName, string $table): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('There exists no unique constraint with the name "%s" on table "%s".', $constraintName, $table),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UnknownColumnOption extends InvalidArgumentException implements SchemaException
|
||||
{
|
||||
public static function new(string $name): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('The "%s" column option is not supported.', $name),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidState;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\Deferrability;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\MatchType;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\ReferentialAction;
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Throwable;
|
||||
use ValueError;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function strrpos;
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* An abstraction class for a foreign key constraint.
|
||||
*
|
||||
* @extends AbstractOptionallyNamedObject<UnqualifiedName>
|
||||
* @final This class will be made final in DBAL 5.0.
|
||||
*/
|
||||
class ForeignKeyConstraint extends AbstractOptionallyNamedObject
|
||||
{
|
||||
/**
|
||||
* Asset identifier instances of the referencing table column names the foreign key constraint is associated with.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var non-empty-array<string, Identifier>
|
||||
*/
|
||||
protected array $_localColumnNames;
|
||||
|
||||
/**
|
||||
* Table or asset identifier instance of the referenced table name the foreign key constraint is associated with.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected Identifier $_foreignTableName;
|
||||
|
||||
/**
|
||||
* Asset identifier instances of the referenced table column names the foreign key constraint is associated with.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var non-empty-array<string, Identifier>
|
||||
*/
|
||||
protected array $_foreignColumnNames;
|
||||
|
||||
/**
|
||||
* Options associated with the foreign key constraint.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $options;
|
||||
|
||||
/**
|
||||
* Referencing table column names the foreign key constraint is associated with.
|
||||
*
|
||||
* An empty list indicates that an attempt to parse column names failed.
|
||||
*
|
||||
* @var list<UnqualifiedName>
|
||||
*/
|
||||
private readonly array $referencingColumnNames;
|
||||
|
||||
/**
|
||||
* Referenced table name the foreign key constraint is associated with.
|
||||
*
|
||||
* A null value indicates that an attempt to parse the table name failed.
|
||||
*/
|
||||
private readonly ?OptionallyQualifiedName $referencedTableName;
|
||||
|
||||
/**
|
||||
* Referenced table column names the foreign key constraint is associated with.
|
||||
*
|
||||
* An empty list indicates that an attempt to parse column names failed.
|
||||
*
|
||||
* @var list<UnqualifiedName>
|
||||
*/
|
||||
private readonly array $referencedColumnNames;
|
||||
|
||||
/**
|
||||
* The match type of the foreign key constraint.
|
||||
*
|
||||
* A null value indicates that an attempt to parse the match type failed.
|
||||
*/
|
||||
private readonly ?MatchType $matchType;
|
||||
|
||||
/**
|
||||
* The referential action for <code>UPDATE</code> operations.
|
||||
*
|
||||
* A null value indicates that an attempt to parse the referential action failed.
|
||||
*/
|
||||
private readonly ?ReferentialAction $onUpdateAction;
|
||||
|
||||
/**
|
||||
* The referential action for <code>DELETE</code> operations.
|
||||
*
|
||||
* A null value indicates that an attempt to parse the referential action failed.
|
||||
*/
|
||||
private readonly ?ReferentialAction $onDeleteAction;
|
||||
|
||||
/**
|
||||
* Indicates whether the constraint is or can be deferred.
|
||||
*
|
||||
* A null value indicates that the combination of the options that defined deferrability was invalid.
|
||||
*/
|
||||
private readonly ?Deferrability $deferrability;
|
||||
|
||||
/**
|
||||
* @internal Use {@link ForeignKeyConstraint::editor()} to instantiate an editor and
|
||||
* {@link ForeignKeyConstraintEditor::create()} to create a foreign key constraint.
|
||||
*
|
||||
* @param non-empty-list<string> $localColumnNames Names of the referencing table columns.
|
||||
* @param string $foreignTableName Referenced table.
|
||||
* @param non-empty-list<string> $foreignColumnNames Names of the referenced table columns.
|
||||
* @param string $name Name of the foreign key constraint.
|
||||
* @param array<string, mixed> $options Options associated with the foreign key constraint.
|
||||
*/
|
||||
public function __construct(
|
||||
array $localColumnNames,
|
||||
string $foreignTableName,
|
||||
array $foreignColumnNames,
|
||||
string $name = '',
|
||||
array $options = [],
|
||||
) {
|
||||
$this->options = $options;
|
||||
|
||||
if (count($localColumnNames) < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Instantiation of a foreign key constraint without local column names is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
if (count($foreignColumnNames) < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Instantiation of a foreign key constraint without foreign column names is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
if (count($foreignColumnNames) !== count($localColumnNames)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Instantiation of a foreign key constraint with a different number of local and foreign'
|
||||
. ' column names is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
parent::__construct($name);
|
||||
|
||||
$this->_localColumnNames = $this->createIdentifierMap($localColumnNames);
|
||||
$this->_foreignTableName = new Identifier($foreignTableName);
|
||||
|
||||
$this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames);
|
||||
|
||||
$this->referencingColumnNames = $this->parseColumnNames($localColumnNames);
|
||||
$this->referencedTableName = $this->parseReferencedTableName($foreignTableName);
|
||||
$this->referencedColumnNames = $this->parseColumnNames($foreignColumnNames);
|
||||
|
||||
$this->matchType = $this->parseMatchType($options);
|
||||
$this->onUpdateAction = $this->parseReferentialAction($options, 'onUpdate');
|
||||
$this->onDeleteAction = $this->parseReferentialAction($options, 'onDelete');
|
||||
|
||||
$this->deferrability = $this->parseDeferrability($options);
|
||||
}
|
||||
|
||||
protected function getNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return Parsers::getUnqualifiedNameParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the referencing table columns the foreign key constraint is associated with.
|
||||
*
|
||||
* @return non-empty-list<UnqualifiedName>
|
||||
*/
|
||||
public function getReferencingColumnNames(): array
|
||||
{
|
||||
if (count($this->referencingColumnNames) < 1) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidReferencingColumnNames($this->getName());
|
||||
}
|
||||
|
||||
return $this->referencingColumnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the referenced table columns the foreign key constraint is associated with.
|
||||
*/
|
||||
public function getReferencedTableName(): OptionallyQualifiedName
|
||||
{
|
||||
if ($this->referencedTableName === null) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidReferencedTableName($this->getName());
|
||||
}
|
||||
|
||||
return $this->referencedTableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the referenced table columns the foreign key constraint is associated with.
|
||||
*
|
||||
* @return non-empty-list<UnqualifiedName>
|
||||
*/
|
||||
public function getReferencedColumnNames(): array
|
||||
{
|
||||
if (count($this->referencedColumnNames) < 1) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidReferencedColumnNames($this->getName());
|
||||
}
|
||||
|
||||
return $this->referencedColumnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the match type of the foreign key constraint.
|
||||
*/
|
||||
public function getMatchType(): MatchType
|
||||
{
|
||||
if ($this->matchType === null) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidMatchType($this->getName());
|
||||
}
|
||||
|
||||
return $this->matchType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referential action for <code>UPDATE</code> operations.
|
||||
*/
|
||||
public function getOnUpdateAction(): ReferentialAction
|
||||
{
|
||||
if ($this->onUpdateAction === null) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidOnUpdateAction($this->getName());
|
||||
}
|
||||
|
||||
return $this->onUpdateAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referential action for <code>DELETE</code> operations.
|
||||
*/
|
||||
public function getOnDeleteAction(): ReferentialAction
|
||||
{
|
||||
if ($this->onDeleteAction === null) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidOnDeleteAction($this->getName());
|
||||
}
|
||||
|
||||
return $this->onDeleteAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the constraint is or can be deferred.
|
||||
*/
|
||||
public function getDeferrability(): Deferrability
|
||||
{
|
||||
if ($this->deferrability === null) {
|
||||
throw InvalidState::foreignKeyConstraintHasInvalidDeferrability($this->getName());
|
||||
}
|
||||
|
||||
return $this->deferrability;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-array<int, string> $names
|
||||
*
|
||||
* @return non-empty-array<string, Identifier>
|
||||
*/
|
||||
private function createIdentifierMap(array $names): array
|
||||
{
|
||||
$identifiers = [];
|
||||
|
||||
foreach ($names as $name) {
|
||||
$identifiers[$name] = new Identifier($name);
|
||||
}
|
||||
|
||||
return $identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the referencing table columns
|
||||
* the foreign key constraint is associated with.
|
||||
*
|
||||
* @deprecated Use {@see getReferencingColumnNames()} instead.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public function getLocalColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencingColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_keys($this->_localColumnNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencingColumnNames()} and {@see UnqualifiedName::toSQL()} instead.
|
||||
*
|
||||
* Returns the quoted representation of the referencing table column names
|
||||
* the foreign key constraint is associated with.
|
||||
*
|
||||
* But only if they were defined with one or the referencing table column name
|
||||
* is a keyword reserved by the platform.
|
||||
* Otherwise the plain unquoted value as inserted is returned.
|
||||
*
|
||||
* @param AbstractPlatform $platform The platform to use for quotation.
|
||||
*
|
||||
* @return non-empty-array<int, string>
|
||||
*/
|
||||
public function getQuotedLocalColumns(AbstractPlatform $platform): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencingColumnNames() and UnqualifiedName::toSQL() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($this->_localColumnNames as $column) {
|
||||
$columns[] = $column->getQuotedName($platform);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencingColumnNames()} instead.
|
||||
*
|
||||
* Returns unquoted representation of local table column names for comparison with other FK
|
||||
*
|
||||
* @return non-empty-array<int, string>
|
||||
*/
|
||||
public function getUnquotedLocalColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencingColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_map($this->trimQuotes(...), $this->getLocalColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedColumnNames()} instead.
|
||||
*
|
||||
* Returns unquoted representation of foreign table column names for comparison with other FK
|
||||
*
|
||||
* @return non-empty-array<int, string>
|
||||
*/
|
||||
public function getUnquotedForeignColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_map($this->trimQuotes(...), $this->getForeignColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedTableName()} instead.
|
||||
*
|
||||
* Returns the name of the referenced table
|
||||
* the foreign key constraint is associated with.
|
||||
*/
|
||||
public function getForeignTableName(): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedTableName() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->_foreignTableName->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedTableName()} instead.
|
||||
*
|
||||
* Returns the non-schema qualified foreign table name.
|
||||
*/
|
||||
public function getUnqualifiedForeignTableName(): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedTableName() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$name = $this->_foreignTableName->getName();
|
||||
$position = strrpos($name, '.');
|
||||
|
||||
if ($position !== false) {
|
||||
$name = substr($name, $position + 1);
|
||||
}
|
||||
|
||||
if ($this->isIdentifierQuoted($name)) {
|
||||
$name = $this->trimQuotes($name);
|
||||
}
|
||||
|
||||
return strtolower($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedTableName()} and {@see OptionallyQualifiedName::toSQL()} instead.
|
||||
*
|
||||
* Returns the quoted representation of the referenced table name
|
||||
* the foreign key constraint is associated with.
|
||||
*
|
||||
* But only if it was defined with one or the referenced table name
|
||||
* is a keyword reserved by the platform.
|
||||
* Otherwise the plain unquoted value as inserted is returned.
|
||||
*
|
||||
* @param AbstractPlatform $platform The platform to use for quotation.
|
||||
*/
|
||||
public function getQuotedForeignTableName(AbstractPlatform $platform): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedTableName() and OptionallyQualifiedName::toSQL() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->_foreignTableName->getQuotedName($platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedColumnNames()} instead.
|
||||
*
|
||||
* Returns the names of the referenced table columns
|
||||
* the foreign key constraint is associated with.
|
||||
*
|
||||
* @return non-empty-array<int, string>
|
||||
*/
|
||||
public function getForeignColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_keys($this->_foreignColumnNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getReferencedColumnNames()} and {@see UnqualifiedName::toSQL()} instead.
|
||||
*
|
||||
* Returns the quoted representation of the referenced table column names
|
||||
* the foreign key constraint is associated with.
|
||||
*
|
||||
* But only if they were defined with one or the referenced table column name
|
||||
* is a keyword reserved by the platform.
|
||||
* Otherwise the plain unquoted value as inserted is returned.
|
||||
*
|
||||
* @param AbstractPlatform $platform The platform to use for quotation.
|
||||
*
|
||||
* @return non-empty-array<int, string>
|
||||
*/
|
||||
public function getQuotedForeignColumns(AbstractPlatform $platform): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getReferencedColumnNames() and UnqualifiedName::toSQL() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($this->_foreignColumnNames as $column) {
|
||||
$columns[] = $column->getQuotedName($platform);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or
|
||||
* {@see getDeferrability()} instead.
|
||||
*
|
||||
* Returns whether or not a given option
|
||||
* is associated with the foreign key constraint.
|
||||
*/
|
||||
public function hasOption(string $name): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or'
|
||||
. ' getDeferrability() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($this->options[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or
|
||||
* {@see getDeferrability()} instead.
|
||||
*
|
||||
* Returns an option associated with the foreign key constraint.
|
||||
*/
|
||||
public function getOption(string $name): mixed
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or'
|
||||
. ' getDeferrability() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getMatchType()}, {@see getOnDeleteAction()}, {@see getOnUpdateAction()} or
|
||||
* {@see getDeferrability()} instead.
|
||||
*
|
||||
* Returns the options associated with the foreign key constraint.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getMatchType(), getOnDeleteAction(), getOnUpdateAction() or'
|
||||
. ' getDeferrability() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getOnUpdateAction()} instead.
|
||||
*
|
||||
* Returns the referential action for UPDATE operations
|
||||
* on the referenced table the foreign key constraint is associated with.
|
||||
*/
|
||||
public function onUpdate(): ?string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getOnUpdateAction() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->onEvent('onUpdate');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getOnDeleteAction()} instead.
|
||||
*
|
||||
* Returns the referential action for DELETE operations
|
||||
* on the referenced table the foreign key constraint is associated with.
|
||||
*/
|
||||
public function onDelete(): ?string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated. Use getOnDeleteAction() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->onEvent('onDelete');
|
||||
}
|
||||
|
||||
private function parseReferencedTableName(string $referencedTableName): ?OptionallyQualifiedName
|
||||
{
|
||||
$parser = Parsers::getOptionallyQualifiedNameParser();
|
||||
|
||||
try {
|
||||
return $parser->parse($referencedTableName);
|
||||
} catch (Throwable $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Unable to parse referenced table name: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $columnNames
|
||||
*
|
||||
* @return list<UnqualifiedName>
|
||||
*/
|
||||
private function parseColumnNames(array $columnNames): array
|
||||
{
|
||||
$parser = Parsers::getUnqualifiedNameParser();
|
||||
|
||||
try {
|
||||
return array_map(
|
||||
static fn (string $columnName) => $parser->parse($columnName),
|
||||
$columnNames,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Unable to parse column name: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
private function parseMatchType(array $options): ?MatchType
|
||||
{
|
||||
if (isset($options['match'])) {
|
||||
try {
|
||||
/**
|
||||
* This looks like a PHPStan bug.
|
||||
*
|
||||
* @phpstan-ignore missingType.checkedException
|
||||
*/
|
||||
return MatchType::from(strtoupper($options['match']));
|
||||
} catch (ValueError $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Unable to parse match type: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return MatchType::SIMPLE;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
private function parseReferentialAction(array $options, string $option): ?ReferentialAction
|
||||
{
|
||||
if (isset($options[$option])) {
|
||||
try {
|
||||
/**
|
||||
* This looks like a PHPStan bug.
|
||||
*
|
||||
* @phpstan-ignore missingType.checkedException
|
||||
*/
|
||||
return ReferentialAction::from(strtoupper($options[$option]));
|
||||
} catch (ValueError $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Unable to parse referential action: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return ReferentialAction::NO_ACTION;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
private function parseDeferrability(array $options): ?Deferrability
|
||||
{
|
||||
// a constraint is INITIALLY IMMEDIATE unless explicitly declared as INITIALLY DEFERRED
|
||||
$isDeferred = isset($options['deferred']) && $options['deferred'] !== false;
|
||||
|
||||
// a constraint is NOT DEFERRABLE unless explicitly declared as DEFERRABLE or is explicitly or implicitly
|
||||
// INITIALLY DEFERRED
|
||||
$isDeferrable = isset($options['deferrable'])
|
||||
? $options['deferrable'] !== false
|
||||
: $isDeferred;
|
||||
|
||||
if ($isDeferred) {
|
||||
if (! $isDeferrable) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'Declaring a constraint as NOT DEFERRABLE INITIALLY DEFERRED is deprecated',
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return Deferrability::DEFERRED;
|
||||
}
|
||||
|
||||
return $isDeferrable ? Deferrability::DEFERRABLE : Deferrability::NOT_DEFERRABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referential action for a given database operation
|
||||
* on the referenced table the foreign key constraint is associated with.
|
||||
*
|
||||
* @param string $event Name of the database operation/event to return the referential action for.
|
||||
*/
|
||||
private function onEvent(string $event): ?string
|
||||
{
|
||||
if (isset($this->options[$event])) {
|
||||
$onEvent = strtoupper($this->options[$event]);
|
||||
|
||||
if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') {
|
||||
return $onEvent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Checks whether this foreign key constraint intersects the given index columns.
|
||||
*
|
||||
* Returns `true` if at least one of this foreign key's local columns
|
||||
* matches one of the given index's columns, `false` otherwise.
|
||||
*
|
||||
* @param Index $index The index to be checked against.
|
||||
*/
|
||||
public function intersectsIndexColumns(Index $index): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6728',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
foreach ($index->getColumns() as $indexColumn) {
|
||||
foreach ($this->_localColumnNames as $localColumn) {
|
||||
if (strtolower($indexColumn) === strtolower($localColumn->getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new foreign key constraint editor.
|
||||
*/
|
||||
public static function editor(): ForeignKeyConstraintEditor
|
||||
{
|
||||
return new ForeignKeyConstraintEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new foreign key constraint editor and initializes it with the constraint's properties.
|
||||
*/
|
||||
public function edit(): ForeignKeyConstraintEditor
|
||||
{
|
||||
return self::editor()
|
||||
->setName($this->getObjectName())
|
||||
->setReferencedTableName($this->getReferencedTableName())
|
||||
->setReferencingColumnNames(...$this->getReferencingColumnNames())
|
||||
->setReferencedColumnNames(...$this->getReferencedColumnNames())
|
||||
->setMatchType($this->getMatchType())
|
||||
->setOnDeleteAction($this->getOnDeleteAction())
|
||||
->setOnUpdateAction($this->getOnUpdateAction())
|
||||
->setDeferrability($this->getDeferrability());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\ForeignKeyConstraint;
|
||||
|
||||
/**
|
||||
* Represents the information about whether the constraint is or can be deferred.
|
||||
*/
|
||||
enum Deferrability: string
|
||||
{
|
||||
case NOT_DEFERRABLE = 'NOT DEFERRABLE';
|
||||
case DEFERRABLE = 'DEFERRABLE';
|
||||
case DEFERRED = 'INITIALLY DEFERRED';
|
||||
|
||||
/**
|
||||
* Returns the SQL representation of the referential action.
|
||||
*/
|
||||
public function toSQL(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\ForeignKeyConstraint;
|
||||
|
||||
/**
|
||||
* Represents the foreign key constraint's match type.
|
||||
*
|
||||
* @link https://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt SQL-92, Subclause 11.8, "<match type>"
|
||||
* @link https://dev.mysql.com/doc/refman/8.4/en/constraint-foreign-key.html
|
||||
* @link https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES
|
||||
* @link https://www.sqlite.org/foreignkeys.html
|
||||
*/
|
||||
enum MatchType: string
|
||||
{
|
||||
case FULL = 'FULL';
|
||||
case PARTIAL = 'PARTIAL';
|
||||
|
||||
/**
|
||||
* The <code>SIMPLE</code> match type is not part of the SQL-92 standard but is supported by and is the default
|
||||
* for MySQL, PostgreSQL and SQLite.
|
||||
*/
|
||||
case SIMPLE = 'SIMPLE';
|
||||
|
||||
/**
|
||||
* Returns the SQL representation of the match type.
|
||||
*/
|
||||
public function toSQL(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\ForeignKeyConstraint;
|
||||
|
||||
/**
|
||||
* Represents the foreign key constraint's referential action.
|
||||
*
|
||||
* @link https://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt SQL-92, Subclause 11.8, "<referential action>"
|
||||
* @link https://dev.mysql.com/doc/refman/8.4/en/constraint-foreign-key.html
|
||||
* @link https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-REFERENCES
|
||||
* @link https://learn.microsoft.com/en-us/sql/relational-databases/tables/primary-and-foreign-key-constraints#cascading-referential-integrity
|
||||
* @link https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/constraint.html
|
||||
* @link https://www.ibm.com/docs/en/db2/11.5?topic=constraints-foreign-key-referential
|
||||
* @link https://www.sqlite.org/foreignkeys.html
|
||||
*/
|
||||
enum ReferentialAction: string
|
||||
{
|
||||
case CASCADE = 'CASCADE';
|
||||
case NO_ACTION = 'NO ACTION';
|
||||
case SET_DEFAULT = 'SET DEFAULT';
|
||||
case SET_NULL = 'SET NULL';
|
||||
|
||||
/**
|
||||
* The <code>RESTRICT</code> referential action is not part of the SQL-92 standard but is supported by MySQL,
|
||||
* PostgreSQL, IBM DB2 and SQLite.
|
||||
*/
|
||||
case RESTRICT = 'RESTRICT';
|
||||
|
||||
/**
|
||||
* Returns the SQL representation of the referential action.
|
||||
*/
|
||||
public function toSQL(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidForeignKeyConstraintDefinition;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\Deferrability;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\MatchType;
|
||||
use Doctrine\DBAL\Schema\ForeignKeyConstraint\ReferentialAction;
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
final class ForeignKeyConstraintEditor
|
||||
{
|
||||
private ?UnqualifiedName $name = null;
|
||||
|
||||
/** @var list<UnqualifiedName> */
|
||||
private array $referencingColumnNames = [];
|
||||
|
||||
private ?OptionallyQualifiedName $referencedTableName = null;
|
||||
|
||||
/** @var list<UnqualifiedName> */
|
||||
private array $referencedColumnNames = [];
|
||||
|
||||
private MatchType $matchType = MatchType::SIMPLE;
|
||||
|
||||
private ReferentialAction $onUpdateAction = ReferentialAction::NO_ACTION;
|
||||
|
||||
private ReferentialAction $onDeleteAction = ReferentialAction::NO_ACTION;
|
||||
|
||||
private Deferrability $deferrability = Deferrability::NOT_DEFERRABLE;
|
||||
|
||||
/**
|
||||
* @internal Use {@link ForeignKeyConstraint::editor()} or {@link ForeignKeyConstraint::edit()} to create
|
||||
* an instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function setName(?UnqualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setUnquotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::unquoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setQuotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::quoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReferencingColumnNames(
|
||||
UnqualifiedName $firstColumnName,
|
||||
UnqualifiedName ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencingColumnNames = [$firstColumnName, ...array_values($otherColumnNames)];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setUnquotedReferencingColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencingColumnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setQuotedReferencingColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencingColumnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReferencedTableName(OptionallyQualifiedName $referencedTableName): self
|
||||
{
|
||||
$this->referencedTableName = $referencedTableName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $unqualifiedReferencedTableName
|
||||
* @param ?non-empty-string $referencedTableNameQualifier
|
||||
*/
|
||||
public function setUnquotedReferencedTableName(
|
||||
string $unqualifiedReferencedTableName,
|
||||
?string $referencedTableNameQualifier = null,
|
||||
): self {
|
||||
$this->referencedTableName =
|
||||
OptionallyQualifiedName::unquoted($unqualifiedReferencedTableName, $referencedTableNameQualifier);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $unqualifiedReferencedTableName
|
||||
* @param ?non-empty-string $referencedTableNameQualifier
|
||||
*/
|
||||
public function setQuotedReferencedTableName(
|
||||
string $unqualifiedReferencedTableName,
|
||||
?string $referencedTableNameQualifier = null,
|
||||
): self {
|
||||
$this->referencedTableName =
|
||||
OptionallyQualifiedName::quoted($unqualifiedReferencedTableName, $referencedTableNameQualifier);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReferencedColumnNames(
|
||||
UnqualifiedName $firstColumnName,
|
||||
UnqualifiedName ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencedColumnNames = [$firstColumnName, ...array_values($otherColumnNames)];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setUnquotedReferencedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencedColumnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setQuotedReferencedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->referencedColumnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMatchType(MatchType $matchType): self
|
||||
{
|
||||
$this->matchType = $matchType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setOnUpdateAction(ReferentialAction $action): self
|
||||
{
|
||||
$this->onUpdateAction = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setOnDeleteAction(ReferentialAction $action): self
|
||||
{
|
||||
$this->onDeleteAction = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDeferrability(Deferrability $deferrability): self
|
||||
{
|
||||
$this->deferrability = $deferrability;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): ForeignKeyConstraint
|
||||
{
|
||||
if (count($this->referencingColumnNames) < 1) {
|
||||
throw InvalidForeignKeyConstraintDefinition::referencingColumnNamesNotSet($this->name);
|
||||
}
|
||||
|
||||
if ($this->referencedTableName === null) {
|
||||
throw InvalidForeignKeyConstraintDefinition::referencedTableNameNotSet($this->name);
|
||||
}
|
||||
|
||||
if (count($this->referencedColumnNames) < 1) {
|
||||
throw InvalidForeignKeyConstraintDefinition::referencedColumnNamesNotSet($this->name);
|
||||
}
|
||||
|
||||
$options = [];
|
||||
|
||||
if ($this->matchType !== MatchType::SIMPLE) {
|
||||
$options['match'] = $this->matchType->value;
|
||||
}
|
||||
|
||||
if ($this->onUpdateAction !== ReferentialAction::NO_ACTION) {
|
||||
$options['onUpdate'] = $this->onUpdateAction->value;
|
||||
}
|
||||
|
||||
if ($this->onDeleteAction !== ReferentialAction::NO_ACTION) {
|
||||
$options['onDelete'] = $this->onDeleteAction->value;
|
||||
}
|
||||
|
||||
return new ForeignKeyConstraint(
|
||||
array_map(
|
||||
static fn (UnqualifiedName $columnName) => $columnName->toString(),
|
||||
$this->referencingColumnNames,
|
||||
),
|
||||
$this->referencedTableName->toString(),
|
||||
array_map(
|
||||
static fn (UnqualifiedName $columnName) => $columnName->toString(),
|
||||
$this->referencedColumnNames,
|
||||
),
|
||||
$this->name?->toString() ?? '',
|
||||
array_merge($options, match ($this->deferrability) {
|
||||
Deferrability::NOT_DEFERRABLE => [],
|
||||
Deferrability::DEFERRABLE => ['deferrable' => true],
|
||||
Deferrability::DEFERRED => ['deferrable' => true, 'deferred' => true],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\GenericName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\GenericNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
|
||||
/**
|
||||
* An abstraction class for an asset identifier.
|
||||
*
|
||||
* Wraps identifier names like column names in indexes / foreign keys
|
||||
* in an abstract class for proper quotation capabilities.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @extends AbstractNamedObject<GenericName>
|
||||
*/
|
||||
class Identifier extends AbstractNamedObject
|
||||
{
|
||||
/**
|
||||
* @param string $identifier Identifier name to wrap.
|
||||
* @param bool $quote Whether to force quoting the given identifier.
|
||||
*/
|
||||
public function __construct(string $identifier, bool $quote = false)
|
||||
{
|
||||
parent::__construct($identifier);
|
||||
|
||||
if (! $quote || $this->_quoted) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_setName('"' . $this->getName() . '"');
|
||||
}
|
||||
|
||||
protected function getNameParser(): GenericNameParser
|
||||
{
|
||||
return Parsers::getGenericNameParser();
|
||||
}
|
||||
}
|
||||
+761
@@ -0,0 +1,761 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidState;
|
||||
use Doctrine\DBAL\Schema\Index\IndexedColumn;
|
||||
use Doctrine\DBAL\Schema\Index\IndexType;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Throwable;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_search;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function gettype;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function strlen;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @final
|
||||
* @extends AbstractNamedObject<UnqualifiedName>
|
||||
*/
|
||||
class Index extends AbstractNamedObject
|
||||
{
|
||||
/**
|
||||
* Asset identifier instances of the column names the index is associated with.
|
||||
*
|
||||
* @deprecated Use {@see getIndexedColumns()} instead.
|
||||
*
|
||||
* @var array<string, Identifier>
|
||||
*/
|
||||
protected array $_columns = [];
|
||||
|
||||
/** @deprecated Use {@see getType()} and compare with {@see IndexType::UNIQUE} instead. */
|
||||
protected bool $_isUnique = false;
|
||||
|
||||
/** @deprecated Use {@see PrimaryKeyConstraint} instead. */
|
||||
protected bool $_isPrimary = false;
|
||||
|
||||
/**
|
||||
* Platform specific flags for indexes.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
protected array $_flags = [];
|
||||
|
||||
/**
|
||||
* Column the index is associated with.
|
||||
*
|
||||
* An empty list indicates that an attempt to parse indexed columns failed.
|
||||
*
|
||||
* @var list<IndexedColumn>
|
||||
*/
|
||||
private readonly array $columns;
|
||||
|
||||
/**
|
||||
* Index type.
|
||||
*
|
||||
* A null value indicates that an attempt to parse the index type failed.
|
||||
*/
|
||||
private ?IndexType $type = null;
|
||||
|
||||
private ?string $predicate = null;
|
||||
|
||||
private bool $failedToParsePredicate = false;
|
||||
|
||||
/**
|
||||
* @internal Use {@link Index::editor()} to instantiate an editor and {@link IndexEditor::create()} to create an
|
||||
* index.
|
||||
*
|
||||
* @param non-empty-list<string> $columns
|
||||
* @param array<int, string> $flags
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(
|
||||
?string $name,
|
||||
array $columns,
|
||||
bool $isUnique = false,
|
||||
bool $isPrimary = false,
|
||||
array $flags = [],
|
||||
private readonly array $options = [],
|
||||
) {
|
||||
parent::__construct($name ?? '');
|
||||
|
||||
if (count($columns) < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6787',
|
||||
'Instantiation of an index without column names is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($isPrimary) {
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6867',
|
||||
'Declaring an index as primary is deprecated. Use PrimaryKeyConstraint instead.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_isUnique = $isUnique || $isPrimary;
|
||||
$this->_isPrimary = $isPrimary;
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$this->_addColumn($column);
|
||||
}
|
||||
|
||||
if (isset($options['where'])) {
|
||||
$predicate = $options['where'];
|
||||
|
||||
if (strlen($predicate) === 0) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'Passing an empty string as index predicate is deprecated.',
|
||||
);
|
||||
|
||||
$this->failedToParsePredicate = true;
|
||||
} else {
|
||||
$this->predicate = $predicate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($flags as $flag) {
|
||||
$this->addFlag($flag);
|
||||
}
|
||||
|
||||
if (count($flags) === 0) {
|
||||
$this->type = $this->inferType();
|
||||
}
|
||||
|
||||
$this->columns = $this->parseColumns($isPrimary, $columns, $options['lengths'] ?? []);
|
||||
}
|
||||
|
||||
protected function getNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return Parsers::getUnqualifiedNameParser();
|
||||
}
|
||||
|
||||
public function getType(): IndexType
|
||||
{
|
||||
if ($this->type === null) {
|
||||
throw InvalidState::indexHasInvalidType($this->getName());
|
||||
}
|
||||
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the indexed columns.
|
||||
*
|
||||
* @return non-empty-list<IndexedColumn>
|
||||
*/
|
||||
public function getIndexedColumns(): array
|
||||
{
|
||||
if (count($this->columns) < 1) {
|
||||
throw InvalidState::indexHasInvalidColumns($this->getName());
|
||||
}
|
||||
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the index is clustered.
|
||||
*/
|
||||
public function isClustered(): bool
|
||||
{
|
||||
return $this->hasFlag('clustered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index predicate.
|
||||
*
|
||||
* @return ?non-empty-string
|
||||
*/
|
||||
public function getPredicate(): ?string
|
||||
{
|
||||
if ($this->failedToParsePredicate) {
|
||||
throw InvalidState::indexHasInvalidPredicate($this->getName());
|
||||
}
|
||||
|
||||
return $this->hasOption('where')
|
||||
? $this->getOption('where')
|
||||
: null;
|
||||
}
|
||||
|
||||
protected function _addColumn(string $column): void
|
||||
{
|
||||
$this->_columns[$column] = new Identifier($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the referencing table columns the constraint is associated with.
|
||||
*
|
||||
* @deprecated Use {@see getIndexedColumns()} instead.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return array_keys($this->_columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quoted representation of the column names the constraint is associated with.
|
||||
*
|
||||
* But only if they were defined with one or a column name
|
||||
* is a keyword reserved by the platform.
|
||||
* Otherwise, the plain unquoted value as inserted is returned.
|
||||
*
|
||||
* @deprecated Use {@see getIndexedColumns()} instead.
|
||||
*
|
||||
* @param AbstractPlatform $platform The platform to use for quotation.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getQuotedColumns(AbstractPlatform $platform): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths')
|
||||
? $this->getOption('lengths') : [];
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($this->_columns as $column) {
|
||||
$length = array_shift($subParts);
|
||||
|
||||
$quotedColumn = $column->getQuotedName($platform);
|
||||
|
||||
if ($length !== null) {
|
||||
$quotedColumn .= '(' . $length . ')';
|
||||
}
|
||||
|
||||
$columns[] = $quotedColumn;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getIndexedColumns()} instead.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public function getUnquotedColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_map($this->trimQuotes(...), $this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the index neither unique nor primary key?
|
||||
*
|
||||
* @deprecated Use {@see getType()} and compare with {@see IndexType::REGULAR} instead.
|
||||
*/
|
||||
public function isSimpleIndex(): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getType() and compare with IndexType::REGULAR instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return ! $this->_isPrimary && ! $this->_isUnique;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see getType()} and compare with {@see IndexType::UNIQUE} instead. */
|
||||
public function isUnique(): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getType() and compare with IndexType::UNIQUE instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->_isUnique;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see PrimaryKeyConstraint} instead. */
|
||||
public function isPrimary(): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6867',
|
||||
'Checking whether an index is primary is deprecated. Use PrimaryKeyConstraint instead.',
|
||||
);
|
||||
|
||||
return $this->_isPrimary;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see getIndexedColumns()} instead. */
|
||||
public function hasColumnAtPosition(string $name, int $pos = 0): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$name = $this->trimQuotes(strtolower($name));
|
||||
$indexColumns = array_map('strtolower', $this->getUnquotedColumns());
|
||||
|
||||
return array_search($name, $indexColumns, true) === $pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this index exactly spans the given column names in the correct order.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param array<int, string> $columnNames
|
||||
*/
|
||||
public function spansColumns(array $columnNames): bool
|
||||
{
|
||||
$columns = $this->getColumns();
|
||||
$numberOfColumns = count($columns);
|
||||
$sameColumns = true;
|
||||
|
||||
for ($i = 0; $i < $numberOfColumns; $i++) {
|
||||
if (
|
||||
isset($columnNames[$i])
|
||||
&& $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i]))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sameColumns = false;
|
||||
}
|
||||
|
||||
return $sameColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the other index already fulfills all the indexing and constraint needs of the current one.
|
||||
*/
|
||||
public function isFulfilledBy(Index $other): bool
|
||||
{
|
||||
// allow the other index to be equally large only. It being larger is an option
|
||||
// but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
|
||||
if (count($other->getColumns()) !== count($this->getColumns())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if columns are the same, and even in the same order
|
||||
$sameColumns = $this->spansColumns($other->getColumns());
|
||||
|
||||
if ($sameColumns) {
|
||||
if (! $this->samePartialIndex($other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->hasSameColumnLengths($other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->isUnique() && ! $this->isPrimary()) {
|
||||
// this is a special case: If the current key is neither primary or unique, any unique or
|
||||
// primary key will always have the same effect for the index and there cannot be any constraint
|
||||
// overlaps. This means a primary or unique index can always fulfill the requirements of just an
|
||||
// index that has no constraints.
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($other->isPrimary() !== $this->isPrimary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $other->isUnique() === $this->isUnique();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the other index is a non-unique, non primary index that can be overwritten by this one.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function overrules(Index $other): bool
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
if ($other->isPrimary()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isSimpleIndex() && $other->isUnique()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->spansColumns($other->getColumns())
|
||||
&& ($this->isPrimary() || $this->isUnique())
|
||||
&& $this->samePartialIndex($other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform specific flags for indexes.
|
||||
*
|
||||
* @deprecated Use {@see getType()} and {@see isClustered()} instead.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getType() and Index::isClustered() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_keys($this->_flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Flag for an index that translates to platform specific handling.
|
||||
*
|
||||
* @deprecated Use {@see edit()}, {@see IndexEditor::setType()} and {@see IndexEditor::setIsClustered()} instead.
|
||||
*
|
||||
* @example $index->addFlag('CLUSTERED')
|
||||
*/
|
||||
public function addFlag(string $flag): self
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::edit(), IndexEditor::setType() and IndexEditor::setIsClustered()'
|
||||
. ' instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$this->_flags[strtolower($flag)] = true;
|
||||
|
||||
$this->validateFlags();
|
||||
|
||||
$this->type = $this->inferType();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this index have a specific flag?
|
||||
*
|
||||
* @deprecated Use {@see getType()} and {@see isClustered()} instead.
|
||||
*/
|
||||
public function hasFlag(string $flag): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getType() and Index::isClustered() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($this->_flags[strtolower($flag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see edit()}, {@see IndexEditor::setType()} and {@see IndexEditor::setIsClustered()}
|
||||
* instead.
|
||||
*/
|
||||
public function removeFlag(string $flag): void
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::edit(), IndexEditor::setType() and IndexEditor::setIsClustered()'
|
||||
. ' instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
unset($this->_flags[strtolower($flag)]);
|
||||
|
||||
$this->type = $this->inferType();
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead. */
|
||||
public function hasOption(string $name): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($this->options[strtolower($name)]);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead. */
|
||||
public function getOption(string $name): mixed
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options[strtolower($name)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getIndexedColumns()} and {@see getPredicate()} instead.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'%s is deprecated. Use Index::getIndexedColumns() and Index::getPredicate() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
private function validateFlags(): void
|
||||
{
|
||||
$unsupportedFlags = $this->_flags;
|
||||
unset(
|
||||
$unsupportedFlags['fulltext'],
|
||||
$unsupportedFlags['spatial'],
|
||||
$unsupportedFlags['clustered'],
|
||||
$unsupportedFlags['nonclustered'],
|
||||
);
|
||||
|
||||
if (count($unsupportedFlags) > 0) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'Configuring an index with non-standard flags is deprecated: %s',
|
||||
implode(', ', array_keys($unsupportedFlags)),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$this->hasFlag('clustered') && (
|
||||
$this->hasFlag('nonclustered')
|
||||
|| $this->hasFlag('fulltext')
|
||||
|| $this->hasFlag('spatial')
|
||||
)
|
||||
) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'A fulltext, spatial or non-clustered index cannot be clustered.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$this->predicate === null
|
||||
|| (! $this->hasFlag('fulltext')
|
||||
&& ! $this->hasFlag('spatial')
|
||||
&& ! $this->hasFlag('clustered'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'A fulltext, spatial or clustered index cannot be partial.',
|
||||
);
|
||||
}
|
||||
|
||||
private function inferType(): ?IndexType
|
||||
{
|
||||
$type = IndexType::REGULAR;
|
||||
$matches = [];
|
||||
|
||||
if ($this->_isUnique) {
|
||||
$type = IndexType::UNIQUE;
|
||||
$matches[] = 'unique';
|
||||
}
|
||||
|
||||
if ($this->hasFlag('fulltext')) {
|
||||
$type = IndexType::FULLTEXT;
|
||||
$matches[] = 'fulltext';
|
||||
}
|
||||
|
||||
if ($this->hasFlag('spatial')) {
|
||||
$type = IndexType::SPATIAL;
|
||||
$matches[] = 'spatial';
|
||||
}
|
||||
|
||||
if (count($matches) > 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6886',
|
||||
'Configuring an index with mutually exclusive properties is deprecated: %s',
|
||||
implode(', ', $matches),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-array<int, string> $columnNames
|
||||
* @param array<int> $lengths
|
||||
*
|
||||
* @return list<IndexedColumn>
|
||||
*/
|
||||
private function parseColumns(bool $isPrimary, array $columnNames, array $lengths): array
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
$parser = Parsers::getUnqualifiedNameParser();
|
||||
|
||||
foreach ($columnNames as $columnName) {
|
||||
try {
|
||||
$parsedName = $parser->parse($columnName);
|
||||
} catch (Throwable $e) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6787',
|
||||
'Unable to parse column name: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$length = array_shift($lengths);
|
||||
|
||||
if ($length !== null) {
|
||||
if ($isPrimary) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6787',
|
||||
'Declaring column length for primary key indexes is deprecated.',
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! is_int($length)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6787',
|
||||
'Indexed column length should be an integer, %s given.',
|
||||
is_object($length) ? $length::class : gettype($length),
|
||||
);
|
||||
|
||||
$length = (int) $length;
|
||||
}
|
||||
|
||||
if ($length < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6787',
|
||||
'Indexed column length should be a positive integer, %d given.',
|
||||
$length,
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
$columns[] = new IndexedColumn($parsedName, $length);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the two indexes have the same partial index
|
||||
*/
|
||||
private function samePartialIndex(Index $other): bool
|
||||
{
|
||||
if (
|
||||
$this->hasOption('where')
|
||||
&& $other->hasOption('where')
|
||||
&& $this->getOption('where') === $other->getOption('where')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! $this->hasOption('where') && ! $other->hasOption('where');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the index has the same column lengths as the other
|
||||
*/
|
||||
private function hasSameColumnLengths(self $other): bool
|
||||
{
|
||||
$filter = static function (?int $length): bool {
|
||||
return $length !== null;
|
||||
};
|
||||
|
||||
return array_filter($this->options['lengths'] ?? [], $filter)
|
||||
=== array_filter($other->options['lengths'] ?? [], $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new index editor.
|
||||
*/
|
||||
public static function editor(): IndexEditor
|
||||
{
|
||||
return new IndexEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new index editor and initializes it with the properties of the current index.
|
||||
*/
|
||||
public function edit(): IndexEditor
|
||||
{
|
||||
return self::editor()
|
||||
->setName($this->getObjectName())
|
||||
->setType($this->getType())
|
||||
->setColumns(...$this->getIndexedColumns())
|
||||
->setIsClustered($this->isClustered())
|
||||
->setPredicate($this->getPredicate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Index;
|
||||
|
||||
enum IndexType
|
||||
{
|
||||
case REGULAR;
|
||||
case UNIQUE;
|
||||
case FULLTEXT;
|
||||
case SPATIAL;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Index;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidIndexDefinition;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
final readonly class IndexedColumn
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param ?positive-int $length
|
||||
*/
|
||||
public function __construct(private UnqualifiedName $columnName, private ?int $length)
|
||||
{
|
||||
if ($length !== null && $length <= 0) {
|
||||
throw InvalidIndexDefinition::fromNonPositiveColumnLength($columnName, $length);
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnName(): UnqualifiedName
|
||||
{
|
||||
return $this->columnName;
|
||||
}
|
||||
|
||||
/** @return ?positive-int */
|
||||
public function getLength(): ?int
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidIndexDefinition;
|
||||
use Doctrine\DBAL\Schema\Index\IndexedColumn;
|
||||
use Doctrine\DBAL\Schema\Index\IndexType;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
final class IndexEditor
|
||||
{
|
||||
private ?UnqualifiedName $name = null;
|
||||
|
||||
private IndexType $type = IndexType::REGULAR;
|
||||
|
||||
/** @var list<IndexedColumn> */
|
||||
private array $columns = [];
|
||||
|
||||
private bool $isClustered = false;
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
private ?string $predicate = null;
|
||||
|
||||
/** @internal Use {@link Index::editor()} or {@link Index::edit()} to create an instance */
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function setName(?UnqualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setUnquotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::unquoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setQuotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::quoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(IndexType $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumns(IndexedColumn $firstColumn, IndexedColumn ...$otherColumns): self
|
||||
{
|
||||
$this->columns = [$firstColumn, ...array_values($otherColumns)];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self
|
||||
{
|
||||
$this->columns = array_map(
|
||||
static fn (UnqualifiedName $name) => new IndexedColumn($name, null),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setUnquotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columns = array_map(
|
||||
static fn (string $name): IndexedColumn => new IndexedColumn(UnqualifiedName::unquoted($name), null),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setQuotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columns = array_map(
|
||||
static fn (string $name): IndexedColumn => new IndexedColumn(UnqualifiedName::quoted($name), null),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsClustered(bool $isClustered): self
|
||||
{
|
||||
$this->isClustered = $isClustered;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param ?non-empty-string $predicate */
|
||||
public function setPredicate(?string $predicate): self
|
||||
{
|
||||
$this->predicate = $predicate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): Index
|
||||
{
|
||||
if ($this->name === null) {
|
||||
throw InvalidIndexDefinition::nameNotSet();
|
||||
}
|
||||
|
||||
if (count($this->columns) < 1) {
|
||||
throw InvalidIndexDefinition::columnsNotSet($this->name);
|
||||
}
|
||||
|
||||
$columnNames = $lengths = $options = $flags = [];
|
||||
foreach ($this->columns as $i => $column) {
|
||||
$columnNames[] = $column->getColumnName()->toString();
|
||||
|
||||
$length = $column->getLength();
|
||||
if ($length === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lengths[$i] = $column->getLength();
|
||||
}
|
||||
|
||||
if (count($lengths) !== 0) {
|
||||
$options['lengths'] = $lengths;
|
||||
}
|
||||
|
||||
if ($this->type === IndexType::FULLTEXT) {
|
||||
$flags[] = 'fulltext';
|
||||
} elseif ($this->type === IndexType::SPATIAL) {
|
||||
$flags[] = 'spatial';
|
||||
}
|
||||
|
||||
if ($this->isClustered) {
|
||||
$flags[] = 'clustered';
|
||||
}
|
||||
|
||||
if ($this->predicate !== null) {
|
||||
$options['where'] = $this->predicate;
|
||||
}
|
||||
|
||||
return new Index(
|
||||
$this->name->toString(),
|
||||
$columnNames,
|
||||
$this->type === IndexType::UNIQUE,
|
||||
false,
|
||||
$flags,
|
||||
$options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\MySQL;
|
||||
use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\CachingCharsetMetadataProvider;
|
||||
use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\ConnectionCharsetMetadataProvider;
|
||||
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
|
||||
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
|
||||
use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function explode;
|
||||
use function func_get_arg;
|
||||
use function func_num_args;
|
||||
use function implode;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function strtr;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* Schema manager for the MySQL RDBMS.
|
||||
*
|
||||
* @extends AbstractSchemaManager<AbstractMySQLPlatform>
|
||||
*/
|
||||
class MySQLSchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
/** @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 ?DefaultTableOptions $defaultTableOptions = null;
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
return $table['TABLE_NAME'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableIndexesList(array $rows, string $tableName): array
|
||||
{
|
||||
foreach ($rows as $i => $row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
|
||||
$row['primary'] = $row['key_name'] === 'PRIMARY';
|
||||
|
||||
if (str_contains($row['index_type'], 'FULLTEXT')) {
|
||||
$row['flags'] = ['FULLTEXT'];
|
||||
} elseif (str_contains($row['index_type'], 'SPATIAL')) {
|
||||
$row['flags'] = ['SPATIAL'];
|
||||
}
|
||||
|
||||
// Ignore prohibited prefix `length` for spatial index
|
||||
if (! str_contains($row['index_type'], 'SPATIAL')) {
|
||||
$row['length'] = isset($row['sub_part']) ? (int) $row['sub_part'] : null;
|
||||
}
|
||||
|
||||
$rows[$i] = $row;
|
||||
}
|
||||
|
||||
return parent::_getPortableTableIndexesList($rows, $tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableDatabaseDefinition(array $database): string
|
||||
{
|
||||
return $database['Database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
|
||||
{
|
||||
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
|
||||
|
||||
$dbType = $tableColumn['type'];
|
||||
$length = null;
|
||||
$scale = 0;
|
||||
$precision = null;
|
||||
$fixed = false;
|
||||
$values = [];
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($dbType);
|
||||
|
||||
switch ($dbType) {
|
||||
case 'char':
|
||||
case 'varchar':
|
||||
$length = (int) $tableColumn['character_maximum_length'];
|
||||
break;
|
||||
|
||||
case 'binary':
|
||||
case 'varbinary':
|
||||
$length = (int) $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 = (int) $tableColumn['numeric_precision'];
|
||||
|
||||
if (isset($tableColumn['numeric_scale'])) {
|
||||
$scale = (int) $tableColumn['numeric_scale'];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($dbType) {
|
||||
case 'char':
|
||||
case 'binary':
|
||||
$fixed = true;
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
$values = $this->parseEnumExpression($tableColumn['column_type']);
|
||||
break;
|
||||
}
|
||||
|
||||
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 ($tableColumn['comment'] !== null) {
|
||||
$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 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],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeysList(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
if (! isset($list[$row['constraint_name']])) {
|
||||
if (! isset($row['delete_rule']) || $row['delete_rule'] === 'RESTRICT') {
|
||||
$row['delete_rule'] = null;
|
||||
}
|
||||
|
||||
if (! isset($row['update_rule']) || $row['update_rule'] === 'RESTRICT') {
|
||||
$row['update_rule'] = null;
|
||||
}
|
||||
|
||||
$list[$row['constraint_name']] = [
|
||||
'name' => $this->getQuotedIdentifierName($row['constraint_name']),
|
||||
'local' => [],
|
||||
'foreign' => [],
|
||||
'foreignTable' => $row['referenced_table_name'],
|
||||
'onDelete' => $row['delete_rule'],
|
||||
'onUpdate' => $row['update_rule'],
|
||||
];
|
||||
}
|
||||
|
||||
$list[$row['constraint_name']]['local'][] = $row['column_name'];
|
||||
$list[$row['constraint_name']]['foreign'][] = $row['referenced_column_name'];
|
||||
}
|
||||
|
||||
return parent::_getPortableTableForeignKeysList($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
return new ForeignKeyConstraint(
|
||||
$tableForeignKey['local'],
|
||||
$tableForeignKey['foreignTable'],
|
||||
$tableForeignKey['foreign'],
|
||||
$tableForeignKey['name'],
|
||||
[
|
||||
'onDelete' => $tableForeignKey['onDelete'],
|
||||
'onUpdate' => $tableForeignKey['onUpdate'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator
|
||||
{
|
||||
return new MySQL\Comparator(
|
||||
$this->platform,
|
||||
new CachingCharsetMetadataProvider(
|
||||
new ConnectionCharsetMetadataProvider($this->connection),
|
||||
),
|
||||
new CachingCollationMetadataProvider(
|
||||
new ConnectionCollationMetadataProvider($this->connection),
|
||||
),
|
||||
$this->getDefaultTableOptions(),
|
||||
func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT TABLE_NAME
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql, [$databaseName]);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
// The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
|
||||
// in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
|
||||
// caused by https://bugs.mysql.com/bug.php?id=81347
|
||||
$conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?'];
|
||||
$params = [$databaseName, $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 't.TABLE_NAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
c.TABLE_NAME,
|
||||
c.COLUMN_NAME AS field,
|
||||
%s AS type,
|
||||
c.COLUMN_TYPE,
|
||||
c.CHARACTER_MAXIMUM_LENGTH,
|
||||
c.CHARACTER_OCTET_LENGTH,
|
||||
c.NUMERIC_PRECISION,
|
||||
c.NUMERIC_SCALE,
|
||||
c.IS_NULLABLE AS `null`,
|
||||
c.COLUMN_KEY AS `key`,
|
||||
c.COLUMN_DEFAULT AS `default`,
|
||||
c.EXTRA,
|
||||
c.COLUMN_COMMENT AS comment,
|
||||
c.CHARACTER_SET_NAME AS characterset,
|
||||
c.COLLATION_NAME AS collation
|
||||
FROM information_schema.COLUMNS c
|
||||
INNER JOIN information_schema.TABLES t
|
||||
ON t.TABLE_NAME = c.TABLE_NAME
|
||||
WHERE %s
|
||||
AND t.TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY c.TABLE_NAME,
|
||||
c.ORDINAL_POSITION
|
||||
SQL,
|
||||
$this->platform->getColumnTypeSQLSnippet('c', $databaseName),
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['TABLE_SCHEMA = ?'];
|
||||
$params = [$databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'TABLE_NAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
NON_UNIQUE AS Non_Unique,
|
||||
INDEX_NAME AS Key_name,
|
||||
COLUMN_NAME AS Column_Name,
|
||||
SUB_PART AS Sub_Part,
|
||||
INDEX_TYPE AS Index_Type
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE %s
|
||||
ORDER BY TABLE_NAME,
|
||||
SEQ_IN_INDEX
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
// The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
|
||||
// in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
|
||||
// caused by https://bugs.mysql.com/bug.php?id=81347
|
||||
$conditions = ['k.TABLE_SCHEMA = ?', 'c.CONSTRAINT_SCHEMA = ?'];
|
||||
$params = [$databaseName, $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'k.TABLE_NAME = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
k.TABLE_NAME,
|
||||
k.CONSTRAINT_NAME,
|
||||
k.COLUMN_NAME,
|
||||
k.REFERENCED_TABLE_NAME,
|
||||
k.REFERENCED_COLUMN_NAME,
|
||||
k.ORDINAL_POSITION,
|
||||
c.UPDATE_RULE,
|
||||
c.DELETE_RULE
|
||||
FROM information_schema.key_column_usage k
|
||||
INNER JOIN information_schema.referential_constraints c
|
||||
ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
|
||||
AND c.TABLE_NAME = k.TABLE_NAME
|
||||
WHERE %s
|
||||
AND k.REFERENCED_COLUMN_NAME IS NOT NULL
|
||||
ORDER BY k.TABLE_NAME,
|
||||
k.CONSTRAINT_NAME,
|
||||
k.ORDINAL_POSITION
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$sql = $this->platform->fetchTableOptionsByTable($tableName !== null);
|
||||
|
||||
$params = [$databaseName];
|
||||
if ($tableName !== null) {
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
/** @var array<non-empty-string,array<string,mixed>> $metadata */
|
||||
$metadata = $this->connection->executeQuery($sql, $params)
|
||||
->fetchAllAssociativeIndexed();
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($metadata as $table => $data) {
|
||||
$data = array_change_key_case($data, CASE_LOWER);
|
||||
|
||||
$tableOptions[$table] = [
|
||||
'engine' => $data['engine'],
|
||||
'collation' => $data['table_collation'],
|
||||
'charset' => $data['character_set_name'],
|
||||
'autoincrement' => $data['auto_increment'],
|
||||
'comment' => $data['table_comment'],
|
||||
'create_options' => $this->parseCreateOptions($data['create_options']),
|
||||
];
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
/** @return array<string, string>|array<string, true> */
|
||||
private function parseCreateOptions(?string $string): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
if ($string === null || $string === '') {
|
||||
return $options;
|
||||
}
|
||||
|
||||
foreach (explode(' ', $string) as $pair) {
|
||||
$parts = explode('=', $pair, 2);
|
||||
|
||||
$options[$parts[0]] = $parts[1] ?? true;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
private function getDefaultTableOptions(): DefaultTableOptions
|
||||
{
|
||||
if ($this->defaultTableOptions === null) {
|
||||
$row = $this->connection->fetchNumeric(
|
||||
'SELECT @@character_set_database, @@collation_database',
|
||||
);
|
||||
|
||||
assert($row !== false);
|
||||
|
||||
$this->defaultTableOptions = new DefaultTableOptions(...$row);
|
||||
}
|
||||
|
||||
return $this->defaultTableOptions;
|
||||
}
|
||||
|
||||
/** Returns the quoted representation of the given identifier name. */
|
||||
private function getQuotedIdentifierName(?string $identifier): ?string
|
||||
{
|
||||
if ($identifier === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->platform->quoteSingleIdentifier($identifier);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
/**
|
||||
* Represents a database object name.
|
||||
*/
|
||||
interface Name
|
||||
{
|
||||
/**
|
||||
* Returns the SQL representation of the name for the given platform.
|
||||
*/
|
||||
public function toSQL(AbstractPlatform $platform): string;
|
||||
|
||||
/**
|
||||
* Returns the string representation of the name.
|
||||
*
|
||||
* If passed to the corresponding parser, the name should be parsed back to an equivalent object.
|
||||
*/
|
||||
public function toString(): string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function implode;
|
||||
|
||||
/**
|
||||
* A generic {@see Name} consisting of one or more identifiers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class GenericName implements Name
|
||||
{
|
||||
/** @var non-empty-list<Identifier> $identifiers */
|
||||
private array $identifiers;
|
||||
|
||||
public function __construct(Identifier $firstIdentifier, Identifier ...$otherIdentifiers)
|
||||
{
|
||||
$this->identifiers = array_merge([$firstIdentifier], array_values($otherIdentifiers));
|
||||
}
|
||||
|
||||
/** @return non-empty-list<Identifier> */
|
||||
public function getIdentifiers(): array
|
||||
{
|
||||
return $this->identifiers;
|
||||
}
|
||||
|
||||
public function toSQL(AbstractPlatform $platform): string
|
||||
{
|
||||
return $this->joinIdentifiers(static fn (Identifier $identifier): string => $identifier->toSQL($platform));
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->joinIdentifiers(static fn (Identifier $identifier): string => $identifier->toString());
|
||||
}
|
||||
|
||||
/** @param callable(Identifier): string $mapper */
|
||||
private function joinIdentifiers(callable $mapper): string
|
||||
{
|
||||
return implode('.', array_map($mapper, $this->identifiers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidIdentifier;
|
||||
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Represents an SQL identifier.
|
||||
*/
|
||||
final readonly class Identifier
|
||||
{
|
||||
/** @param non-empty-string $value */
|
||||
private function __construct(
|
||||
private string $value,
|
||||
private bool $isQuoted,
|
||||
) {
|
||||
if (strlen($this->value) === 0) {
|
||||
throw InvalidIdentifier::fromEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return non-empty-string */
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isQuoted(): bool
|
||||
{
|
||||
return $this->isQuoted;
|
||||
}
|
||||
|
||||
public function toSQL(AbstractPlatform $platform): string
|
||||
{
|
||||
return $platform->quoteSingleIdentifier(
|
||||
$this->toNormalizedValue($platform->getUnquotedIdentifierFolding()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the literal value of the identifier normalized according to the rules of the given database platform.
|
||||
*
|
||||
* Consumers should use the normalized value for schema comparison and referencing the objects to be introspected.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function toNormalizedValue(UnquotedIdentifierFolding $folding): string
|
||||
{
|
||||
if (! $this->isQuoted) {
|
||||
return $folding->foldUnquotedIdentifier($this->value);
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
if (! $this->isQuoted) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
return sprintf('"%s"', str_replace('"', '""', $this->value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a quoted identifier.
|
||||
*
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public static function quoted(string $value): self
|
||||
{
|
||||
return new self($value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unquoted identifier.
|
||||
*
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public static function unquoted(string $value): self
|
||||
{
|
||||
return new self($value, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Name;
|
||||
|
||||
/**
|
||||
* An optionally qualified {@see Name} consisting of an unqualified name and an optional unqualified qualifier.
|
||||
*/
|
||||
final readonly class OptionallyQualifiedName implements Name
|
||||
{
|
||||
public function __construct(private Identifier $unqualifiedName, private ?Identifier $qualifier)
|
||||
{
|
||||
}
|
||||
|
||||
public function getUnqualifiedName(): Identifier
|
||||
{
|
||||
return $this->unqualifiedName;
|
||||
}
|
||||
|
||||
public function getQualifier(): ?Identifier
|
||||
{
|
||||
return $this->qualifier;
|
||||
}
|
||||
|
||||
public function toSQL(AbstractPlatform $platform): string
|
||||
{
|
||||
$unqualifiedName = $this->unqualifiedName->toSQL($platform);
|
||||
|
||||
if ($this->qualifier === null) {
|
||||
return $unqualifiedName;
|
||||
}
|
||||
|
||||
return $this->qualifier->toSQL($platform) . '.' . $unqualifiedName;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$unqualifiedName = $this->unqualifiedName->toString();
|
||||
|
||||
if ($this->qualifier === null) {
|
||||
return $unqualifiedName;
|
||||
}
|
||||
|
||||
return $this->qualifier->toString() . '.' . $unqualifiedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an optionally qualified name with all identifiers quoted.
|
||||
*
|
||||
* @param non-empty-string $unqualifiedName
|
||||
* @param ?non-empty-string $qualifier
|
||||
*/
|
||||
public static function quoted(string $unqualifiedName, ?string $qualifier = null): self
|
||||
{
|
||||
return new self(
|
||||
Identifier::quoted($unqualifiedName),
|
||||
$qualifier !== null ? Identifier::quoted($qualifier) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an optionally qualified name with all identifiers unquoted.
|
||||
*
|
||||
* @param non-empty-string $unqualifiedName
|
||||
* @param ?non-empty-string $qualifier
|
||||
*/
|
||||
public static function unquoted(string $unqualifiedName, ?string $qualifier = null): self
|
||||
{
|
||||
return new self(
|
||||
Identifier::unquoted($unqualifiedName),
|
||||
$qualifier !== null ? Identifier::unquoted($qualifier) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
|
||||
/**
|
||||
* Parses a database object name.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
interface Parser
|
||||
{
|
||||
/**
|
||||
* @return N
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function parse(string $input): Name;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface Exception extends Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
class ExpectedDot extends LogicException implements Exception
|
||||
{
|
||||
public static function new(int $position, string $got): self
|
||||
{
|
||||
return new self(sprintf('Expected dot at position %d, got "%s".', $position, $got));
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
class ExpectedNextIdentifier extends LogicException implements Exception
|
||||
{
|
||||
public static function new(): self
|
||||
{
|
||||
return new self('Unexpected end of input. Next identifier expected.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
class InvalidName extends InvalidArgumentException implements Exception
|
||||
{
|
||||
public static function forUnqualifiedName(int $count): self
|
||||
{
|
||||
return new self(sprintf('An unqualified name must consist of one identifier, %d given.', $count));
|
||||
}
|
||||
|
||||
public static function forOptionallyQualifiedName(int $count): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('An optionally qualified name must consist of one or two identifiers, %d given.', $count),
|
||||
);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception;
|
||||
use LogicException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @internal */
|
||||
class UnableToParseIdentifier extends LogicException implements Exception
|
||||
{
|
||||
public static function new(int $offset): self
|
||||
{
|
||||
return new self(sprintf('Unable to parse identifier at offset %d.', $offset));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\GenericName;
|
||||
use Doctrine\DBAL\Schema\Name\Identifier;
|
||||
use Doctrine\DBAL\Schema\Name\Parser;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception\ExpectedDot;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception\ExpectedNextIdentifier;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception\UnableToParseIdentifier;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function preg_match;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Parses a generic qualified or unqualified SQL-like name.
|
||||
*
|
||||
* A name can be either unqualified or qualified:
|
||||
* - An unqualified name consists of a single identifier.
|
||||
* - A qualified name is a sequence of two or more identifiers separated by dots.
|
||||
*
|
||||
* An identifier can be quoted or unquoted:
|
||||
* - A quoted identifier is enclosed in double quotes ("), backticks (`), or square brackets ([]).
|
||||
* The closing quote character can be escaped by doubling it.
|
||||
* - An unquoted identifier may contain any character except whitespace, dots, or any of the quote characters.
|
||||
*
|
||||
* Differences from SQL:
|
||||
* 1. Identifiers that are reserved keywords or start with a digit do not need to be quoted.
|
||||
* 2. Whitespace is not allowed between identifiers.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @implements Parser<GenericName>
|
||||
*/
|
||||
final class GenericNameParser implements Parser
|
||||
{
|
||||
private const IDENTIFIER_PATTERN = <<<'PATTERN'
|
||||
/\G
|
||||
(?:
|
||||
"(?<ansi>[^"]*(?:""[^"]*)*)" # ANSI SQL double-quoted
|
||||
| `(?<mysql>[^`]*(?:``[^`]*)*)` # MySQL-style backtick-quoted
|
||||
| \[(?<sqlserver>[^]]*(?:]][^]]*)*)] # SQL Server-style square-bracket-quoted
|
||||
| (?<unquoted>[^\s."`\[\]]+) # Unquoted
|
||||
)
|
||||
/x
|
||||
PATTERN;
|
||||
|
||||
public function parse(string $input): GenericName
|
||||
{
|
||||
$offset = 0;
|
||||
$identifiers = [];
|
||||
$length = strlen($input);
|
||||
|
||||
while (true) {
|
||||
if ($offset >= $length) {
|
||||
throw ExpectedNextIdentifier::new();
|
||||
}
|
||||
|
||||
if (preg_match(self::IDENTIFIER_PATTERN, $input, $matches, 0, $offset) === 0) {
|
||||
throw UnableToParseIdentifier::new($offset);
|
||||
}
|
||||
|
||||
if (isset($matches['ansi']) && strlen($matches['ansi']) > 0) {
|
||||
$identifier = Identifier::quoted(str_replace('""', '"', $matches['ansi']));
|
||||
} elseif (isset($matches['mysql']) && strlen($matches['mysql']) > 0) {
|
||||
$identifier = Identifier::quoted(str_replace('``', '`', $matches['mysql']));
|
||||
} elseif (isset($matches['sqlserver']) && strlen($matches['sqlserver']) > 0) {
|
||||
$identifier = Identifier::quoted(str_replace(']]', ']', $matches['sqlserver']));
|
||||
} else {
|
||||
assert(isset($matches['unquoted']) && strlen($matches['unquoted']) > 0);
|
||||
$identifier = Identifier::unquoted($matches['unquoted']);
|
||||
}
|
||||
|
||||
$identifiers[] = $identifier;
|
||||
|
||||
$offset += strlen($matches[0]);
|
||||
|
||||
if ($offset >= $length) {
|
||||
break;
|
||||
}
|
||||
|
||||
$character = $input[$offset];
|
||||
|
||||
if ($character !== '.') {
|
||||
throw ExpectedDot::new($offset, $character);
|
||||
}
|
||||
|
||||
$offset++;
|
||||
}
|
||||
|
||||
assert(count($identifiers) > 0);
|
||||
|
||||
return new GenericName(...$identifiers);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception\InvalidName;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @implements Parser<OptionallyQualifiedName>
|
||||
*/
|
||||
final readonly class OptionallyQualifiedNameParser implements Parser
|
||||
{
|
||||
public function __construct(private GenericNameParser $genericNameParser)
|
||||
{
|
||||
}
|
||||
|
||||
public function parse(string $input): OptionallyQualifiedName
|
||||
{
|
||||
$identifiers = $this->genericNameParser->parse($input)
|
||||
->getIdentifiers();
|
||||
|
||||
return match (count($identifiers)) {
|
||||
1 => new OptionallyQualifiedName($identifiers[0], null),
|
||||
2 => new OptionallyQualifiedName($identifiers[1], $identifiers[0]),
|
||||
default => throw InvalidName::forOptionallyQualifiedName(count($identifiers)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name\Parser;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\Exception\InvalidName;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @implements Parser<UnqualifiedName>
|
||||
*/
|
||||
final readonly class UnqualifiedNameParser implements Parser
|
||||
{
|
||||
public function __construct(private GenericNameParser $genericNameParser)
|
||||
{
|
||||
}
|
||||
|
||||
public function parse(string $input): UnqualifiedName
|
||||
{
|
||||
$identifiers = $this->genericNameParser->parse($input)
|
||||
->getIdentifiers();
|
||||
|
||||
if (count($identifiers) > 1) {
|
||||
throw InvalidName::forUnqualifiedName(count($identifiers));
|
||||
}
|
||||
|
||||
return new UnqualifiedName($identifiers[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\Parser\GenericNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\OptionallyQualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
|
||||
/**
|
||||
* A static registry for name parsers.
|
||||
*
|
||||
* @internal This class should be used by {@link AbstractAsset} subclasses only.
|
||||
*/
|
||||
final class Parsers
|
||||
{
|
||||
private static ?UnqualifiedNameParser $unqualifiedNameParser = null;
|
||||
|
||||
private static ?OptionallyQualifiedNameParser $optionallyQualifiedNameParser = null;
|
||||
|
||||
private static ?GenericNameParser $genericNameParser = null;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getUnqualifiedNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return self::$unqualifiedNameParser ??= new UnqualifiedNameParser(self::getGenericNameParser());
|
||||
}
|
||||
|
||||
public static function getOptionallyQualifiedNameParser(): OptionallyQualifiedNameParser
|
||||
{
|
||||
return self::$optionallyQualifiedNameParser ??= new OptionallyQualifiedNameParser(self::getGenericNameParser());
|
||||
}
|
||||
|
||||
public static function getGenericNameParser(): GenericNameParser
|
||||
{
|
||||
return self::$genericNameParser ??= new GenericNameParser();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Name;
|
||||
|
||||
/**
|
||||
* An unqualified {@see Name} consisting of a single identifier.
|
||||
*/
|
||||
final readonly class UnqualifiedName implements Name
|
||||
{
|
||||
public function __construct(private Identifier $identifier)
|
||||
{
|
||||
}
|
||||
|
||||
public function getIdentifier(): Identifier
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function toSQL(AbstractPlatform $platform): string
|
||||
{
|
||||
return $this->identifier->toSQL($platform);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->identifier->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a quoted unqualified name.
|
||||
*
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public static function quoted(string $value): self
|
||||
{
|
||||
return new self(Identifier::quoted($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unquoted unqualified name.
|
||||
*
|
||||
* @param non-empty-string $value
|
||||
*/
|
||||
public static function unquoted(string $value): self
|
||||
{
|
||||
return new self(Identifier::unquoted($value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema\Name;
|
||||
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
|
||||
/**
|
||||
* Defines how a database platform folds the case of unquoted identifiers.
|
||||
*/
|
||||
enum UnquotedIdentifierFolding
|
||||
{
|
||||
/**
|
||||
* Represents upper-case folding of unquoted identifiers.
|
||||
*/
|
||||
case UPPER;
|
||||
|
||||
/**
|
||||
* Represents lower-case folding of unquoted identifiers.
|
||||
*/
|
||||
case LOWER;
|
||||
|
||||
/**
|
||||
* Represents no folding of unquoted identifiers.
|
||||
*/
|
||||
case NONE;
|
||||
|
||||
/**
|
||||
* Applies case folding to an unquoted identifier as a database platform would when processing an SQL statement.
|
||||
*
|
||||
* @param non-empty-string $value
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function foldUnquotedIdentifier(string $value): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::UPPER => strtoupper($value),
|
||||
self::LOWER => strtolower($value),
|
||||
self::NONE => $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
/**
|
||||
* A database object that has a {@see Name}.
|
||||
*
|
||||
* This interface is intentionally designed to conflict with {@see OptionallyNamedObject}.
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
interface NamedObject
|
||||
{
|
||||
/**
|
||||
* Returns the object name.
|
||||
*
|
||||
* @return N
|
||||
*/
|
||||
public function getObjectName(): Name;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
/**
|
||||
* A database object that optionally has a {@see Name}.
|
||||
*
|
||||
* This interface is intentionally designed to conflict with {@see NamedObject}.
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
interface OptionallyNamedObject
|
||||
{
|
||||
/**
|
||||
* Returns the object name or <code>null</code>, if the name is not set.
|
||||
*
|
||||
* @return ?N
|
||||
*/
|
||||
public function getObjectName(): ?Name;
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
|
||||
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function array_key_exists;
|
||||
use function assert;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
use function trim;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* Oracle Schema Manager.
|
||||
*
|
||||
* @extends AbstractSchemaManager<OraclePlatform>
|
||||
*/
|
||||
class OracleSchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
$view = array_change_key_case($view, CASE_LOWER);
|
||||
|
||||
return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
$table = array_change_key_case($table, CASE_LOWER);
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return $this->getQuotedIdentifierName($table['table_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableIndexesList(array $rows, string $tableName): array
|
||||
{
|
||||
$indexBuffer = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
|
||||
$buffer = [];
|
||||
|
||||
if ($row['is_primary'] === 'P') {
|
||||
$buffer['key_name'] = 'primary';
|
||||
$buffer['primary'] = true;
|
||||
$buffer['non_unique'] = false;
|
||||
} else {
|
||||
$buffer['key_name'] = strtolower($row['name']);
|
||||
$buffer['primary'] = false;
|
||||
$buffer['non_unique'] = ! $row['is_unique'];
|
||||
}
|
||||
|
||||
$buffer['column_name'] = $this->getQuotedIdentifierName($row['column_name']);
|
||||
$indexBuffer[] = $buffer;
|
||||
}
|
||||
|
||||
return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
|
||||
{
|
||||
$tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
|
||||
|
||||
$dbType = strtolower($tableColumn['data_type']);
|
||||
if (str_starts_with($dbType, 'timestamp(')) {
|
||||
if (str_contains($dbType, 'with time zone')) {
|
||||
$dbType = 'timestamptz';
|
||||
} else {
|
||||
$dbType = 'timestamp';
|
||||
}
|
||||
}
|
||||
|
||||
$length = $precision = null;
|
||||
$scale = 0;
|
||||
$fixed = false;
|
||||
|
||||
assert(array_key_exists('data_default', $tableColumn));
|
||||
|
||||
// Default values returned from database sometimes have trailing spaces.
|
||||
if (is_string($tableColumn['data_default'])) {
|
||||
$tableColumn['data_default'] = trim($tableColumn['data_default']);
|
||||
}
|
||||
|
||||
if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
|
||||
$tableColumn['data_default'] = null;
|
||||
}
|
||||
|
||||
if ($tableColumn['data_default'] !== null) {
|
||||
// Default values returned from database are represented as literal expressions
|
||||
if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) {
|
||||
$tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($tableColumn['data_precision'] !== null) {
|
||||
$precision = (int) $tableColumn['data_precision'];
|
||||
}
|
||||
|
||||
if ($tableColumn['data_scale'] !== null) {
|
||||
$scale = (int) $tableColumn['data_scale'];
|
||||
}
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($dbType);
|
||||
|
||||
switch ($dbType) {
|
||||
case 'number':
|
||||
if ($precision === 20 && $scale === 0) {
|
||||
$type = 'bigint';
|
||||
} elseif ($precision === 5 && $scale === 0) {
|
||||
$type = 'smallint';
|
||||
} elseif ($precision === 1 && $scale === 0) {
|
||||
$type = 'boolean';
|
||||
} elseif ($scale > 0) {
|
||||
$type = 'decimal';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'float':
|
||||
if ($precision === 63) {
|
||||
$type = 'smallfloat';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'varchar':
|
||||
case 'varchar2':
|
||||
case 'nvarchar2':
|
||||
$length = (int) $tableColumn['char_length'];
|
||||
break;
|
||||
|
||||
case 'raw':
|
||||
$length = (int) $tableColumn['data_length'];
|
||||
$fixed = true;
|
||||
break;
|
||||
|
||||
case 'char':
|
||||
case 'nchar':
|
||||
$length = (int) $tableColumn['char_length'];
|
||||
$fixed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'notnull' => $tableColumn['nullable'] === 'N',
|
||||
'fixed' => $fixed,
|
||||
'default' => $tableColumn['data_default'],
|
||||
'length' => $length,
|
||||
'precision' => $precision,
|
||||
'scale' => $scale,
|
||||
];
|
||||
|
||||
if ($tableColumn['comments'] !== null) {
|
||||
$options['comment'] = $tableColumn['comments'];
|
||||
}
|
||||
|
||||
return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeysList(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
if (! isset($list[$row['constraint_name']])) {
|
||||
if ($row['delete_rule'] === 'NO ACTION') {
|
||||
$row['delete_rule'] = null;
|
||||
}
|
||||
|
||||
$list[$row['constraint_name']] = [
|
||||
'name' => $this->getQuotedIdentifierName($row['constraint_name']),
|
||||
'local' => [],
|
||||
'foreign' => [],
|
||||
'foreignTable' => $row['references_table'],
|
||||
'onDelete' => $row['delete_rule'],
|
||||
'deferrable' => $row['deferrable'] === 'DEFERRABLE',
|
||||
'deferred' => $row['deferred'] === 'DEFERRED',
|
||||
];
|
||||
}
|
||||
|
||||
$localColumn = $this->getQuotedIdentifierName($row['local_column']);
|
||||
$foreignColumn = $this->getQuotedIdentifierName($row['foreign_column']);
|
||||
|
||||
$list[$row['constraint_name']]['local'][] = $localColumn;
|
||||
$list[$row['constraint_name']]['foreign'][] = $foreignColumn;
|
||||
}
|
||||
|
||||
return parent::_getPortableTableForeignKeysList($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
return new ForeignKeyConstraint(
|
||||
$tableForeignKey['local'],
|
||||
$this->getQuotedIdentifierName($tableForeignKey['foreignTable']),
|
||||
$tableForeignKey['foreign'],
|
||||
$this->getQuotedIdentifierName($tableForeignKey['name']),
|
||||
[
|
||||
'onDelete' => $tableForeignKey['onDelete'],
|
||||
'deferrable' => $tableForeignKey['deferrable'],
|
||||
'deferred' => $tableForeignKey['deferred'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableSequenceDefinition(array $sequence): Sequence
|
||||
{
|
||||
$sequence = array_change_key_case($sequence, CASE_LOWER);
|
||||
|
||||
return new Sequence(
|
||||
$this->getQuotedIdentifierName($sequence['sequence_name']),
|
||||
(int) $sequence['increment_by'],
|
||||
(int) $sequence['min_value'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableDatabaseDefinition(array $database): string
|
||||
{
|
||||
$database = array_change_key_case($database, CASE_LOWER);
|
||||
|
||||
return $database['username'];
|
||||
}
|
||||
|
||||
public function createDatabase(string $database): void
|
||||
{
|
||||
$statement = $this->platform->getCreateDatabaseSQL($database);
|
||||
|
||||
$params = $this->connection->getParams();
|
||||
|
||||
if (isset($params['password'])) {
|
||||
$statement .= ' IDENTIFIED BY ' . $this->connection->quoteSingleIdentifier($params['password']);
|
||||
}
|
||||
|
||||
$this->connection->executeStatement($statement);
|
||||
|
||||
$statement = 'GRANT DBA TO ' . $database;
|
||||
$this->connection->executeStatement($statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal The method should be only used by the {@see OracleSchemaManager} class.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function dropAutoincrement(string $table): bool
|
||||
{
|
||||
$sql = $this->platform->getDropAutoincrementSql($table);
|
||||
foreach ($sql as $query) {
|
||||
$this->connection->executeStatement($query);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function dropTable(string $name): void
|
||||
{
|
||||
try {
|
||||
$this->dropAutoincrement($name);
|
||||
} catch (DatabaseObjectNotFoundException) {
|
||||
}
|
||||
|
||||
parent::dropTable($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the quoted representation of the given identifier name.
|
||||
*
|
||||
* Quotes non-uppercase identifiers explicitly to preserve case
|
||||
* and thus make references to the particular identifier work.
|
||||
*/
|
||||
private function getQuotedIdentifierName(string $identifier): string
|
||||
{
|
||||
if (preg_match('/[a-z]/', $identifier) === 1) {
|
||||
return $this->platform->quoteSingleIdentifier($identifier);
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT TABLE_NAME
|
||||
FROM ALL_TABLES
|
||||
WHERE OWNER = :OWNER
|
||||
ORDER BY TABLE_NAME
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql, ['OWNER' => $databaseName]);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['C.OWNER = :OWNER'];
|
||||
$params = ['OWNER' => $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'C.TABLE_NAME = :TABLE_NAME';
|
||||
$params['TABLE_NAME'] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
C.TABLE_NAME,
|
||||
C.COLUMN_NAME,
|
||||
C.DATA_TYPE,
|
||||
C.DATA_DEFAULT,
|
||||
C.DATA_PRECISION,
|
||||
C.DATA_SCALE,
|
||||
C.CHAR_LENGTH,
|
||||
C.DATA_LENGTH,
|
||||
C.NULLABLE,
|
||||
D.COMMENTS
|
||||
FROM ALL_TAB_COLUMNS C
|
||||
INNER JOIN ALL_TABLES T
|
||||
ON T.OWNER = C.OWNER
|
||||
AND T.TABLE_NAME = C.TABLE_NAME
|
||||
LEFT JOIN ALL_COL_COMMENTS D
|
||||
ON D.OWNER = C.OWNER
|
||||
AND D.TABLE_NAME = C.TABLE_NAME
|
||||
AND D.COLUMN_NAME = C.COLUMN_NAME
|
||||
WHERE %s
|
||||
ORDER BY C.TABLE_NAME, C.COLUMN_ID
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ['IND_COL.INDEX_OWNER = :OWNER'];
|
||||
$params = ['OWNER' => $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME';
|
||||
$params['TABLE_NAME'] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
IND_COL.TABLE_NAME,
|
||||
IND_COL.INDEX_NAME AS NAME,
|
||||
IND.INDEX_TYPE AS TYPE,
|
||||
DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE,
|
||||
IND_COL.COLUMN_NAME,
|
||||
IND_COL.COLUMN_POSITION AS COLUMN_POS,
|
||||
CON.CONSTRAINT_TYPE AS IS_PRIMARY
|
||||
FROM ALL_IND_COLUMNS IND_COL
|
||||
LEFT JOIN ALL_INDEXES IND
|
||||
ON IND.OWNER = IND_COL.INDEX_OWNER
|
||||
AND IND.INDEX_NAME = IND_COL.INDEX_NAME
|
||||
LEFT JOIN ALL_CONSTRAINTS CON
|
||||
ON CON.OWNER = IND_COL.INDEX_OWNER
|
||||
AND CON.INDEX_NAME = IND_COL.INDEX_NAME
|
||||
WHERE %s
|
||||
ORDER BY IND_COL.TABLE_NAME,
|
||||
IND_COL.INDEX_NAME,
|
||||
IND_COL.COLUMN_POSITION
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER'];
|
||||
$params = ['OWNER' => $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME';
|
||||
$params['TABLE_NAME'] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
COLS.TABLE_NAME,
|
||||
ALC.CONSTRAINT_NAME,
|
||||
ALC.DELETE_RULE,
|
||||
ALC.DEFERRABLE,
|
||||
ALC.DEFERRED,
|
||||
COLS.COLUMN_NAME LOCAL_COLUMN,
|
||||
COLS.POSITION,
|
||||
R_COLS.TABLE_NAME REFERENCES_TABLE,
|
||||
R_COLS.COLUMN_NAME FOREIGN_COLUMN
|
||||
FROM ALL_CONS_COLUMNS COLS
|
||||
LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
|
||||
LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND
|
||||
R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND
|
||||
R_COLS.POSITION = COLS.POSITION
|
||||
WHERE %s
|
||||
ORDER BY COLS.TABLE_NAME,
|
||||
COLS.CONSTRAINT_NAME,
|
||||
COLS.POSITION
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$conditions = ['OWNER = :OWNER'];
|
||||
$params = ['OWNER' => $databaseName];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 'TABLE_NAME = :TABLE_NAME';
|
||||
$params['TABLE_NAME'] = $tableName;
|
||||
}
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT TABLE_NAME,
|
||||
COMMENTS
|
||||
FROM ALL_TAB_COMMENTS
|
||||
WHERE %s
|
||||
ORDER BY TABLE_NAME
|
||||
SQL,
|
||||
implode(' AND ', $conditions),
|
||||
);
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($this->connection->iterateKeyValue($sql, $params) as $table => $comments) {
|
||||
$tableOptions[$table] = ['comment' => $comments];
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@see Identifier::toNormalizedValue()} instead. */
|
||||
protected function normalizeName(string $name): string
|
||||
{
|
||||
$identifier = new Identifier($name);
|
||||
|
||||
return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\JsonType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* PostgreSQL Schema Manager.
|
||||
*
|
||||
* @extends AbstractSchemaManager<PostgreSQLPlatform>
|
||||
*/
|
||||
class PostgreSQLSchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function listSchemaNames(): array
|
||||
{
|
||||
return $this->connection->fetchFirstColumn(
|
||||
<<<'SQL'
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT LIKE 'pg\_%'
|
||||
AND schema_name != 'information_schema'
|
||||
SQL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the current schema.
|
||||
*
|
||||
* @deprecated Use {@link getCurrentSchemaName()} instead
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getCurrentSchema(): ?string
|
||||
{
|
||||
return $this->getCurrentSchemaName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the name of the current schema.
|
||||
*
|
||||
* @deprecated Use {@link determineCurrentSchemaName()} instead
|
||||
*
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function determineCurrentSchema(): string
|
||||
{
|
||||
$currentSchema = $this->connection->fetchOne('SELECT current_schema()');
|
||||
assert(is_string($currentSchema));
|
||||
assert(strlen($currentSchema) > 0);
|
||||
|
||||
return $currentSchema;
|
||||
}
|
||||
|
||||
protected function determineCurrentSchemaName(): ?string
|
||||
{
|
||||
return $this->determineCurrentSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
$onUpdate = null;
|
||||
$onDelete = null;
|
||||
|
||||
if (
|
||||
preg_match(
|
||||
'(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
|
||||
$tableForeignKey['condef'],
|
||||
$match,
|
||||
) === 1
|
||||
) {
|
||||
$onUpdate = $match[1];
|
||||
}
|
||||
|
||||
if (
|
||||
preg_match(
|
||||
'(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
|
||||
$tableForeignKey['condef'],
|
||||
$match,
|
||||
) === 1
|
||||
) {
|
||||
$onDelete = $match[1];
|
||||
}
|
||||
|
||||
$result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values);
|
||||
assert($result === 1);
|
||||
|
||||
// PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
|
||||
// the idea to trim them here.
|
||||
$localColumns = array_map('trim', explode(',', $values[1]));
|
||||
$foreignColumns = array_map('trim', explode(',', $values[3]));
|
||||
$foreignTable = $values[2];
|
||||
|
||||
return new ForeignKeyConstraint(
|
||||
$localColumns,
|
||||
$foreignTable,
|
||||
$foreignColumns,
|
||||
$tableForeignKey['conname'],
|
||||
[
|
||||
'onUpdate' => $onUpdate,
|
||||
'onDelete' => $onDelete,
|
||||
'deferrable' => (bool) $tableForeignKey['condeferrable'],
|
||||
'deferred' => (bool) $tableForeignKey['condeferred'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
// @phpstan-ignore missingType.checkedException
|
||||
$currentSchema = $this->getCurrentSchema();
|
||||
|
||||
if ($table['schema_name'] === $currentSchema) {
|
||||
return $table['table_name'];
|
||||
}
|
||||
|
||||
return $table['schema_name'] . '.' . $table['table_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableIndexesList(array $rows, string $tableName): array
|
||||
{
|
||||
return parent::_getPortableTableIndexesList(array_map(
|
||||
/** @param array<string, mixed> $row */
|
||||
static function (array $row): array {
|
||||
return [
|
||||
'key_name' => $row['relname'],
|
||||
'non_unique' => ! $row['indisunique'],
|
||||
'primary' => (bool) $row['indisprimary'],
|
||||
'where' => $row['where'],
|
||||
'column_name' => $row['attname'],
|
||||
];
|
||||
},
|
||||
$rows,
|
||||
), $tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableDatabaseDefinition(array $database): string
|
||||
{
|
||||
return $database['datname'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableSequenceDefinition(array $sequence): Sequence
|
||||
{
|
||||
if ($sequence['schemaname'] !== 'public') {
|
||||
$sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
|
||||
} else {
|
||||
$sequenceName = $sequence['relname'];
|
||||
}
|
||||
|
||||
return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(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'];
|
||||
}
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($dbType);
|
||||
|
||||
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 the parameters between parenthesis in the data type.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a default value expression as given by PostgreSQL
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT quote_ident(table_name) AS table_name,
|
||||
table_schema AS schema_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_catalog = ?
|
||||
AND table_schema NOT LIKE 'pg\_%'
|
||||
AND table_schema != 'information_schema'
|
||||
AND table_name != 'geometry_columns'
|
||||
AND table_name != 'spatial_ref_sys'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY
|
||||
quote_ident(table_name)
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql, [$databaseName]);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT quote_ident(n.nspname) AS schema_name,
|
||||
quote_ident(c.relname) AS table_name,
|
||||
quote_ident(a.attname) AS field,
|
||||
t.typname AS type,
|
||||
format_type(a.atttypid, a.atttypmod) AS complete_type,
|
||||
bt.typname AS domain_type,
|
||||
format_type(bt.oid, t.typtypmod) AS domain_complete_type,
|
||||
a.attnotnull AS isnotnull,
|
||||
a.attidentity,
|
||||
(%s) AS "default",
|
||||
dsc.description AS comment,
|
||||
CASE
|
||||
WHEN coll.collprovider = 'c'
|
||||
THEN coll.collcollate
|
||||
WHEN coll.collprovider = 'd'
|
||||
THEN NULL
|
||||
ELSE coll.collname
|
||||
END AS collation
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c
|
||||
ON c.oid = a.attrelid
|
||||
JOIN pg_namespace n
|
||||
ON n.oid = c.relnamespace
|
||||
JOIN pg_type t
|
||||
ON t.oid = a.atttypid
|
||||
LEFT JOIN pg_type bt
|
||||
ON t.typtype = 'd'
|
||||
AND bt.oid = t.typbasetype
|
||||
LEFT JOIN pg_collation coll
|
||||
ON coll.oid = a.attcollation
|
||||
LEFT JOIN pg_depend dep
|
||||
ON dep.objid = c.oid
|
||||
AND dep.deptype = 'e'
|
||||
AND dep.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
|
||||
LEFT JOIN pg_description dsc
|
||||
ON dsc.objoid = c.oid AND dsc.objsubid = a.attnum
|
||||
LEFT JOIN pg_inherits i
|
||||
ON i.inhrelid = c.oid
|
||||
LEFT JOIN pg_class p
|
||||
ON i.inhparent = p.oid
|
||||
AND p.relkind = 'p'
|
||||
WHERE %s
|
||||
-- 'r' for regular tables - 'p' for partitioned tables
|
||||
AND c.relkind IN ('r', 'p')
|
||||
AND a.attnum > 0
|
||||
AND dep.refobjid IS NULL
|
||||
-- exclude partitions (tables that inherit from partitioned tables)
|
||||
AND p.oid IS NULL
|
||||
ORDER BY n.nspname,
|
||||
c.relname,
|
||||
a.attnum
|
||||
SQL,
|
||||
$this->platform->getDefaultColumnValueSQLSnippet(),
|
||||
implode(' AND ', $this->buildQueryConditions($tableName, $params)),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
quote_ident(n.nspname) AS schema_name,
|
||||
quote_ident(c.relname) AS table_name,
|
||||
quote_ident(ic.relname) AS relname,
|
||||
i.indisunique,
|
||||
i.indisprimary,
|
||||
i.indkey,
|
||||
i.indrelid,
|
||||
pg_get_expr(indpred, indrelid) AS "where",
|
||||
quote_ident(attname) AS attname
|
||||
FROM pg_index i
|
||||
JOIN pg_class AS c ON c.oid = i.indrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
JOIN pg_class AS ic ON ic.oid = i.indexrelid
|
||||
JOIN LATERAL UNNEST(i.indkey) WITH ORDINALITY AS keys(attnum, ord)
|
||||
ON TRUE
|
||||
JOIN pg_attribute a
|
||||
ON a.attrelid = c.oid
|
||||
AND a.attnum = keys.attnum
|
||||
WHERE %s
|
||||
ORDER BY 1, 2, keys.ord;
|
||||
SQL,
|
||||
implode(' AND ', $this->buildQueryConditions($tableName, $params)),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
quote_ident(tn.nspname) AS schema_name,
|
||||
quote_ident(tc.relname) AS table_name,
|
||||
quote_ident(r.conname) as conname,
|
||||
pg_get_constraintdef(r.oid, true) as condef,
|
||||
r.condeferrable,
|
||||
r.condeferred
|
||||
FROM pg_constraint r
|
||||
JOIN pg_class AS tc ON tc.oid = r.conrelid
|
||||
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
||||
WHERE r.conrelid IN
|
||||
(
|
||||
SELECT c.oid
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n
|
||||
ON n.oid = c.relnamespace
|
||||
WHERE %s)
|
||||
AND r.contype = 'f'
|
||||
ORDER BY 1, 2
|
||||
SQL,
|
||||
implode(' AND ', $this->buildQueryConditions($tableName, $params)),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT quote_ident(n.nspname) AS schema_name,
|
||||
quote_ident(c.relname) AS table_name,
|
||||
CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
|
||||
obj_description(c.oid, 'pg_class') AS comment
|
||||
FROM pg_class c
|
||||
INNER JOIN pg_namespace n
|
||||
ON n.oid = c.relnamespace
|
||||
WHERE
|
||||
c.relkind = 'r'
|
||||
AND %s
|
||||
SQL,
|
||||
implode(' AND ', $this->buildQueryConditions($tableName, $params)),
|
||||
);
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($this->connection->iterateAssociative($sql, $params) as $row) {
|
||||
$tableOptions[$this->_getPortableTableDefinition($row)] = $row;
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int|string> $params
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
private function buildQueryConditions(?string $tableName, array &$params): array
|
||||
{
|
||||
$conditions = [];
|
||||
|
||||
if ($tableName !== null) {
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$schemaName, $tableName] = explode('.', $tableName);
|
||||
|
||||
$conditions[] = 'n.nspname = ?';
|
||||
$params[] = $schemaName;
|
||||
} else {
|
||||
$conditions[] = 'n.nspname = ANY(current_schemas(false))';
|
||||
}
|
||||
|
||||
$conditions[] = 'c.relname = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
$conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
|
||||
|
||||
return $conditions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidPrimaryKeyConstraintDefinition;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function count;
|
||||
|
||||
/** @implements OptionallyNamedObject<UnqualifiedName> */
|
||||
final readonly class PrimaryKeyConstraint implements OptionallyNamedObject
|
||||
{
|
||||
/**
|
||||
* @internal Use {@link PrimaryKeyConstraint::editor()} to instantiate an editor and
|
||||
* {@link PrimaryKeyConstraintEditor::create()} to create a primary key constraint.
|
||||
*
|
||||
* @param ?UnqualifiedName $name Name of the primary key constraint. If omitted in the schema
|
||||
* defined by the application, it is considered that the name is
|
||||
* not essential and may be generated by the underlying database
|
||||
* platform.
|
||||
* @param non-empty-list<UnqualifiedName> $columnNames
|
||||
*/
|
||||
public function __construct(
|
||||
private ?UnqualifiedName $name,
|
||||
private array $columnNames,
|
||||
private bool $isClustered,
|
||||
) {
|
||||
if (count($this->columnNames) < 1) {
|
||||
throw InvalidPrimaryKeyConstraintDefinition::columnNamesNotSet();
|
||||
}
|
||||
}
|
||||
|
||||
public function getObjectName(): ?UnqualifiedName
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the columns.
|
||||
*
|
||||
* @return non-empty-list<UnqualifiedName>
|
||||
*/
|
||||
public function getColumnNames(): array
|
||||
{
|
||||
return $this->columnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the primary key constraint is clustered.
|
||||
*/
|
||||
public function isClustered(): bool
|
||||
{
|
||||
return $this->isClustered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new primary key constraint editor.
|
||||
*/
|
||||
public static function editor(): PrimaryKeyConstraintEditor
|
||||
{
|
||||
return new PrimaryKeyConstraintEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new foreign key constraint editor and initializes it with the constraint's properties.
|
||||
*/
|
||||
public function edit(): PrimaryKeyConstraintEditor
|
||||
{
|
||||
return self::editor()
|
||||
->setName($this->name)
|
||||
->setColumnNames(...$this->columnNames)
|
||||
->setIsClustered($this->isClustered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidPrimaryKeyConstraintDefinition;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
final class PrimaryKeyConstraintEditor
|
||||
{
|
||||
private ?UnqualifiedName $name = null;
|
||||
|
||||
/** @var list<UnqualifiedName> */
|
||||
private array $columnNames = [];
|
||||
|
||||
private bool $isClustered = true;
|
||||
|
||||
/**
|
||||
* @internal Use {@link PrimaryKeyConstraint::editor()} or {@link PrimaryKeyConstraint::edit()} to create
|
||||
* an instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function setName(?UnqualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setUnquotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::unquoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setQuotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::quoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self
|
||||
{
|
||||
$this->columnNames = [$firstColumnName, ...array_values($otherColumnNames)];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setUnquotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setQuotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name),
|
||||
[$firstColumnName, ...array_values($otherColumnNames)],
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsClustered(bool $isClustered): self
|
||||
{
|
||||
$this->isClustered = $isClustered;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): PrimaryKeyConstraint
|
||||
{
|
||||
if (count($this->columnNames) < 1) {
|
||||
throw InvalidPrimaryKeyConstraintDefinition::columnNamesNotSet();
|
||||
}
|
||||
|
||||
return new PrimaryKeyConstraint($this->name, $this->columnNames, $this->isClustered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\SQLServer;
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function assert;
|
||||
use function explode;
|
||||
use function func_get_arg;
|
||||
use function func_num_args;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* SQL Server Schema Manager.
|
||||
*
|
||||
* @extends AbstractSchemaManager<SQLServerPlatform>
|
||||
*/
|
||||
class SQLServerSchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
private ?string $databaseCollation = null;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function listSchemaNames(): array
|
||||
{
|
||||
return $this->connection->fetchFirstColumn(
|
||||
<<<'SQL'
|
||||
SELECT name
|
||||
FROM sys.schemas
|
||||
WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')
|
||||
SQL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableSequenceDefinition(array $sequence): Sequence
|
||||
{
|
||||
return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
|
||||
{
|
||||
$dbType = $tableColumn['type'];
|
||||
|
||||
$length = (int) $tableColumn['length'];
|
||||
|
||||
$precision = null;
|
||||
|
||||
$scale = 0;
|
||||
$fixed = false;
|
||||
|
||||
if ($tableColumn['scale'] !== null) {
|
||||
$scale = (int) $tableColumn['scale'];
|
||||
}
|
||||
|
||||
if ($tableColumn['precision'] !== null) {
|
||||
$precision = (int) $tableColumn['precision'];
|
||||
}
|
||||
|
||||
switch ($dbType) {
|
||||
case 'nchar':
|
||||
case 'ntext':
|
||||
// Unicode data requires 2 bytes per character
|
||||
$length /= 2;
|
||||
break;
|
||||
|
||||
case 'nvarchar':
|
||||
if ($length === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Unicode data requires 2 bytes per character
|
||||
$length /= 2;
|
||||
break;
|
||||
|
||||
case 'varchar':
|
||||
// TEXT type is returned as VARCHAR(MAX) with a length of -1
|
||||
if ($length === -1) {
|
||||
$dbType = 'text';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'varbinary':
|
||||
if ($length === -1) {
|
||||
$dbType = 'blob';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
|
||||
$fixed = true;
|
||||
}
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($dbType);
|
||||
|
||||
$options = [
|
||||
'fixed' => $fixed,
|
||||
'notnull' => (bool) $tableColumn['notnull'],
|
||||
'scale' => $scale,
|
||||
'precision' => $precision,
|
||||
'autoincrement' => (bool) $tableColumn['autoincrement'],
|
||||
];
|
||||
|
||||
if ($tableColumn['comment'] !== null) {
|
||||
$options['comment'] = $tableColumn['comment'];
|
||||
}
|
||||
|
||||
if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) {
|
||||
$options['length'] = $length;
|
||||
}
|
||||
|
||||
$column = new Column($tableColumn['name'], Type::getType($type), $options);
|
||||
|
||||
if ($tableColumn['default'] !== null) {
|
||||
$default = $this->parseDefaultExpression($tableColumn['default']);
|
||||
|
||||
$column->setDefault($default);
|
||||
$column->setPlatformOption(
|
||||
SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME,
|
||||
$tableColumn['df_name'],
|
||||
);
|
||||
}
|
||||
|
||||
$column->setPlatformOption('collation', $tableColumn['collation']);
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
private function parseDefaultExpression(string $value): ?string
|
||||
{
|
||||
while (preg_match('/^\((.*)\)$/s', $value, $matches) === 1) {
|
||||
$value = $matches[1];
|
||||
}
|
||||
|
||||
if ($value === 'NULL') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) {
|
||||
$value = str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
|
||||
if ($value === 'getdate()') {
|
||||
return $this->platform->getCurrentTimestampSQL();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeysList(array $rows): array
|
||||
{
|
||||
$foreignKeys = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$name = $row['ForeignKey'];
|
||||
|
||||
if (! isset($foreignKeys[$name])) {
|
||||
$referencedTableName = $row['ReferenceTableName'];
|
||||
|
||||
// @phpstan-ignore missingType.checkedException
|
||||
if ($row['ReferenceSchemaName'] !== $this->getCurrentSchemaName()) {
|
||||
$referencedTableName = $row['ReferenceSchemaName'] . '.' . $referencedTableName;
|
||||
}
|
||||
|
||||
$foreignKeys[$name] = [
|
||||
'local_columns' => [$row['ColumnName']],
|
||||
'foreign_table' => $referencedTableName,
|
||||
'foreign_columns' => [$row['ReferenceColumnName']],
|
||||
'name' => $name,
|
||||
'options' => [
|
||||
'onUpdate' => str_replace('_', ' ', $row['update_referential_action_desc']),
|
||||
'onDelete' => str_replace('_', ' ', $row['delete_referential_action_desc']),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$foreignKeys[$name]['local_columns'][] = $row['ColumnName'];
|
||||
$foreignKeys[$name]['foreign_columns'][] = $row['ReferenceColumnName'];
|
||||
}
|
||||
}
|
||||
|
||||
return parent::_getPortableTableForeignKeysList($foreignKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableIndexesList(array $rows, string $tableName): array
|
||||
{
|
||||
foreach ($rows as &$row) {
|
||||
$row['non_unique'] = (bool) $row['non_unique'];
|
||||
$row['primary'] = (bool) $row['primary'];
|
||||
$row['flags'] = $row['flags'] ? [$row['flags']] : null;
|
||||
}
|
||||
|
||||
return parent::_getPortableTableIndexesList($rows, $tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
return new ForeignKeyConstraint(
|
||||
$tableForeignKey['local_columns'],
|
||||
$tableForeignKey['foreign_table'],
|
||||
$tableForeignKey['foreign_columns'],
|
||||
$tableForeignKey['name'],
|
||||
$tableForeignKey['options'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
// @phpstan-ignore missingType.checkedException
|
||||
if ($table['schema_name'] !== $this->getCurrentSchemaName()) {
|
||||
return $table['schema_name'] . '.' . $table['table_name'];
|
||||
}
|
||||
|
||||
return $table['table_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableDatabaseDefinition(array $database): string
|
||||
{
|
||||
return $database['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
return new View($view['name'], $view['definition']);
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator
|
||||
{
|
||||
return new SQLServer\Comparator(
|
||||
$this->platform,
|
||||
$this->getDatabaseCollation(),
|
||||
func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
private function getDatabaseCollation(): string
|
||||
{
|
||||
if ($this->databaseCollation === null) {
|
||||
$databaseCollation = $this->connection->fetchOne(
|
||||
'SELECT collation_name FROM sys.databases WHERE name = '
|
||||
. $this->platform->getCurrentDatabaseExpression(),
|
||||
);
|
||||
|
||||
// a database is always selected, even if omitted in the connection parameters
|
||||
assert(is_string($databaseCollation));
|
||||
|
||||
$this->databaseCollation = $databaseCollation;
|
||||
}
|
||||
|
||||
return $this->databaseCollation;
|
||||
}
|
||||
|
||||
protected function determineCurrentSchemaName(): ?string
|
||||
{
|
||||
$schemaName = $this->connection->fetchOne('SELECT SCHEMA_NAME()');
|
||||
assert($schemaName !== false);
|
||||
|
||||
return $schemaName;
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
// The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
|
||||
$sql = <<<'SQL'
|
||||
SELECT SCHEMA_NAME(schema_id) AS schema_name,
|
||||
name AS table_name
|
||||
FROM sys.tables
|
||||
WHERE name != 'sysdiagrams'
|
||||
ORDER BY name
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
scm.name AS schema_name,
|
||||
tbl.name AS table_name,
|
||||
col.name,
|
||||
type.name AS type,
|
||||
col.max_length AS length,
|
||||
~col.is_nullable AS notnull,
|
||||
def.definition AS [default],
|
||||
def.name AS df_name,
|
||||
col.scale,
|
||||
col.precision,
|
||||
col.is_identity AS autoincrement,
|
||||
col.collation_name AS collation,
|
||||
-- CAST avoids driver error for sql_variant type
|
||||
CAST(prop.value AS NVARCHAR(MAX)) AS comment
|
||||
FROM sys.columns AS col
|
||||
JOIN sys.types AS type
|
||||
ON col.user_type_id = type.user_type_id
|
||||
JOIN sys.tables AS tbl
|
||||
ON col.object_id = tbl.object_id
|
||||
JOIN sys.schemas AS scm
|
||||
ON tbl.schema_id = scm.schema_id
|
||||
LEFT JOIN sys.default_constraints def
|
||||
ON col.default_object_id = def.object_id
|
||||
AND col.object_id = def.parent_object_id
|
||||
LEFT JOIN sys.extended_properties AS prop
|
||||
ON tbl.object_id = prop.major_id
|
||||
AND col.column_id = prop.minor_id
|
||||
AND prop.name = 'MS_Description'
|
||||
WHERE %s
|
||||
ORDER BY scm.name,
|
||||
tbl.name,
|
||||
col.column_id
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
scm.name AS schema_name,
|
||||
tbl.name AS table_name,
|
||||
idx.name AS key_name,
|
||||
col.name AS column_name,
|
||||
~idx.is_unique AS non_unique,
|
||||
idx.is_primary_key AS [primary],
|
||||
CASE idx.type
|
||||
WHEN '1' THEN 'clustered'
|
||||
WHEN '2' THEN 'nonclustered'
|
||||
ELSE NULL
|
||||
END AS flags
|
||||
FROM sys.tables AS tbl
|
||||
JOIN sys.schemas AS scm
|
||||
ON tbl.schema_id = scm.schema_id
|
||||
JOIN sys.indexes AS idx
|
||||
ON tbl.object_id = idx.object_id
|
||||
JOIN sys.index_columns AS idxcol
|
||||
ON idx.object_id = idxcol.object_id
|
||||
AND idx.index_id = idxcol.index_id
|
||||
JOIN sys.columns AS col
|
||||
ON idxcol.object_id = col.object_id
|
||||
AND idxcol.column_id = col.column_id
|
||||
WHERE %s
|
||||
ORDER BY scm.name,
|
||||
tbl.name,
|
||||
idx.index_id,
|
||||
idxcol.key_ordinal
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
SCHEMA_NAME(f.schema_id) AS schema_name,
|
||||
OBJECT_NAME(f.parent_object_id) AS table_name,
|
||||
f.name AS ForeignKey,
|
||||
COL_NAME(fc.parent_object_id, fc.parent_column_id) AS ColumnName,
|
||||
SCHEMA_NAME(t.schema_id) ReferenceSchemaName,
|
||||
OBJECT_NAME(f.referenced_object_id) AS ReferenceTableName,
|
||||
COL_NAME(fc.referenced_object_id, fc.referenced_column_id) AS ReferenceColumnName,
|
||||
f.delete_referential_action_desc,
|
||||
f.update_referential_action_desc
|
||||
FROM sys.foreign_keys AS f
|
||||
INNER JOIN sys.foreign_key_columns AS fc
|
||||
ON f.object_id = fc.constraint_object_id
|
||||
INNER JOIN sys.tables AS t
|
||||
ON t.object_id = fc.referenced_object_id
|
||||
WHERE %s
|
||||
ORDER BY 1,
|
||||
2,
|
||||
3,
|
||||
fc.constraint_column_id
|
||||
SQL,
|
||||
$this->getWhereClause(
|
||||
$tableName,
|
||||
'SCHEMA_NAME(f.schema_id)',
|
||||
'OBJECT_NAME(f.parent_object_id)',
|
||||
$params,
|
||||
),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
scm.name AS schema_name,
|
||||
tbl.name AS table_name,
|
||||
p.value
|
||||
FROM
|
||||
sys.tables AS tbl
|
||||
JOIN sys.schemas AS scm
|
||||
ON tbl.schema_id = scm.schema_id
|
||||
INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
|
||||
WHERE
|
||||
p.name = N'MS_Description'
|
||||
AND %s
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, 'scm.name', 'tbl.name', $params),
|
||||
);
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($this->connection->iterateAssociative($sql, $params) as $data) {
|
||||
$data = array_change_key_case($data, CASE_LOWER);
|
||||
|
||||
$tableOptions[$this->_getPortableTableDefinition($data)] = [
|
||||
'comment' => $data['value'],
|
||||
];
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the where clause to filter schema and table name in a query.
|
||||
*
|
||||
* @param ?string $tableName The full qualified name of the table.
|
||||
* @param string $schemaColumn The name of the column to compare the schema to in the where clause.
|
||||
* @param string $tableColumn The name of the column to compare the table to in the where clause.
|
||||
* @param list<string> $params
|
||||
*/
|
||||
private function getWhereClause(
|
||||
?string $tableName,
|
||||
string $schemaColumn,
|
||||
string $tableColumn,
|
||||
array &$params,
|
||||
): string {
|
||||
$conditions = [];
|
||||
|
||||
if ($tableName !== null) {
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$schemaName, $tableName] = explode('.', $tableName);
|
||||
|
||||
$conditions = [sprintf('%s = ?', $schemaColumn)];
|
||||
$params[] = $schemaName;
|
||||
} else {
|
||||
$conditions = [sprintf('%s = SCHEMA_NAME()', $schemaColumn)];
|
||||
}
|
||||
|
||||
$conditions[] = sprintf('%s = ?', $tableColumn);
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
// The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
|
||||
$conditions[] = sprintf("%s != 'sysdiagrams'", $tableColumn);
|
||||
|
||||
return implode(' AND ', $conditions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\SQLite;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function array_column;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function func_get_arg;
|
||||
use function func_num_args;
|
||||
use function implode;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function preg_quote;
|
||||
use function preg_replace;
|
||||
use function rtrim;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function str_replace;
|
||||
use function strcasecmp;
|
||||
use function strtolower;
|
||||
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* SQLite SchemaManager.
|
||||
*
|
||||
* @extends AbstractSchemaManager<SQLitePlatform>
|
||||
*/
|
||||
class SQLiteSchemaManager extends AbstractSchemaManager
|
||||
{
|
||||
public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void
|
||||
{
|
||||
$table = $this->introspectTable($table);
|
||||
|
||||
$this->alterTable(new TableDiff($table, addedForeignKeys: [$foreignKey]));
|
||||
}
|
||||
|
||||
public function dropForeignKey(string $name, string $table): void
|
||||
{
|
||||
$table = $this->introspectTable($table);
|
||||
|
||||
$foreignKey = $table->getForeignKey($name);
|
||||
|
||||
$this->alterTable(new TableDiff($table, droppedForeignKeys: [$foreignKey]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the schema name and the unqualified table name separately instead.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableDefinition(array $table): string
|
||||
{
|
||||
return $table['table_name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableColumnDefinition(array $tableColumn): Column
|
||||
{
|
||||
$matchResult = preg_match('/^([A-Z\s]+?)(?:\s*\((\d+)(?:,\s*(\d+))?\))?$/i', $tableColumn['type'], $matches);
|
||||
assert($matchResult === 1);
|
||||
|
||||
$dbType = strtolower($matches[1]);
|
||||
|
||||
$length = $precision = null;
|
||||
$fixed = $unsigned = false;
|
||||
$scale = 0;
|
||||
|
||||
if (isset($matches[2])) {
|
||||
if (isset($matches[3])) {
|
||||
$precision = (int) $matches[2];
|
||||
$scale = (int) $matches[3];
|
||||
} else {
|
||||
$length = (int) $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (str_contains($dbType, ' unsigned')) {
|
||||
$dbType = str_replace(' unsigned', '', $dbType);
|
||||
$unsigned = true;
|
||||
}
|
||||
|
||||
$type = $this->platform->getDoctrineTypeMapping($dbType);
|
||||
$default = $tableColumn['dflt_value'];
|
||||
if ($default === 'NULL') {
|
||||
$default = null;
|
||||
}
|
||||
|
||||
if ($default !== null) {
|
||||
// SQLite returns the default value as a literal expression, so we need to parse it
|
||||
if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
|
||||
$default = str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$notnull = (bool) $tableColumn['notnull'];
|
||||
|
||||
if ($dbType === 'char') {
|
||||
$fixed = true;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'autoincrement' => $tableColumn['autoincrement'],
|
||||
'comment' => $tableColumn['comment'],
|
||||
'length' => $length,
|
||||
'unsigned' => $unsigned,
|
||||
'fixed' => $fixed,
|
||||
'notnull' => $notnull,
|
||||
'default' => $default,
|
||||
'precision' => $precision,
|
||||
'scale' => $scale,
|
||||
];
|
||||
|
||||
$column = new Column($tableColumn['name'], Type::getType($type), $options);
|
||||
|
||||
if ($type === Types::STRING || $type === Types::TEXT) {
|
||||
$column->setPlatformOption('collation', $tableColumn['collation'] ?? 'BINARY');
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableViewDefinition(array $view): View
|
||||
{
|
||||
return new View($view['name'], $view['sql']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeysList(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
$id = $row['id'];
|
||||
if (! isset($list[$id])) {
|
||||
if (! isset($row['on_delete']) || $row['on_delete'] === 'RESTRICT') {
|
||||
$row['on_delete'] = null;
|
||||
}
|
||||
|
||||
if (! isset($row['on_update']) || $row['on_update'] === 'RESTRICT') {
|
||||
$row['on_update'] = null;
|
||||
}
|
||||
|
||||
$list[$id] = [
|
||||
'name' => $row['constraint_name'],
|
||||
'local' => [],
|
||||
'foreign' => [],
|
||||
'foreignTable' => $row['table'],
|
||||
'onDelete' => $row['on_delete'],
|
||||
'onUpdate' => $row['on_update'],
|
||||
'deferrable' => $row['deferrable'],
|
||||
'deferred' => $row['deferred'],
|
||||
];
|
||||
}
|
||||
|
||||
$list[$id]['local'][] = $row['from'];
|
||||
|
||||
if ($row['to'] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$id]['foreign'][] = $row['to'];
|
||||
}
|
||||
|
||||
foreach ($list as $id => $value) {
|
||||
if (count($value['foreign']) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inferring a shorthand form for the foreign key constraint, where the "to" field is empty.
|
||||
// @see https://www.sqlite.org/foreignkeys.html#fk_indexes.
|
||||
// @phpstan-ignore missingType.checkedException
|
||||
$foreignTablePrimaryKeyColumnRows = $this->fetchPrimaryKeyColumns($value['foreignTable']);
|
||||
|
||||
if (count($foreignTablePrimaryKeyColumnRows) < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6701',
|
||||
'Introspection of SQLite foreign key constraints with omitted referenced column names'
|
||||
. ' in an incomplete schema is deprecated.',
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$id]['foreign'] = array_column($foreignTablePrimaryKeyColumnRows, 'name');
|
||||
}
|
||||
|
||||
return parent::_getPortableTableForeignKeysList($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
|
||||
{
|
||||
return new ForeignKeyConstraint(
|
||||
$tableForeignKey['local'],
|
||||
$tableForeignKey['foreignTable'],
|
||||
$tableForeignKey['foreign'],
|
||||
$tableForeignKey['name'],
|
||||
[
|
||||
'onDelete' => $tableForeignKey['onDelete'],
|
||||
'onUpdate' => $tableForeignKey['onUpdate'],
|
||||
'deferrable' => $tableForeignKey['deferrable'],
|
||||
'deferred' => $tableForeignKey['deferred'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function parseColumnCollationFromSQL(string $column, string $sql): ?string
|
||||
{
|
||||
$pattern = '{' . $this->buildIdentifierPattern($column)
|
||||
. '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
|
||||
|
||||
if (preg_match($pattern, $sql, $match) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $match[1];
|
||||
}
|
||||
|
||||
private function parseTableCommentFromSQL(string $table, string $sql): ?string
|
||||
{
|
||||
$pattern = '/\s* # Allow whitespace characters at start of line
|
||||
CREATE\sTABLE' . $this->buildIdentifierPattern($table) . '
|
||||
( # Start capture
|
||||
(?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
|
||||
)/ix';
|
||||
|
||||
if (preg_match($pattern, $sql, $match) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
|
||||
|
||||
return $comment === '' ? null : $comment;
|
||||
}
|
||||
|
||||
private function parseColumnCommentFromSQL(string $column, string $sql): string
|
||||
{
|
||||
$pattern = '{[\s(,]' . $this->buildIdentifierPattern($column)
|
||||
. '(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
|
||||
|
||||
if (preg_match($pattern, $sql, $match) !== 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
|
||||
assert(is_string($comment));
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a regular expression pattern that matches the given unquoted or quoted identifier.
|
||||
*/
|
||||
private function buildIdentifierPattern(string $identifier): string
|
||||
{
|
||||
return '(?:' . implode('|', array_map(
|
||||
static function (string $sql): string {
|
||||
return '\W' . preg_quote($sql, '/') . '\W';
|
||||
},
|
||||
[
|
||||
$identifier,
|
||||
$this->platform->quoteSingleIdentifier($identifier),
|
||||
],
|
||||
)) . ')';
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
private function getCreateTableSQL(string $table): string
|
||||
{
|
||||
$sql = $this->connection->fetchOne(
|
||||
<<<'SQL'
|
||||
SELECT sql
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM sqlite_master
|
||||
UNION ALL
|
||||
SELECT *
|
||||
FROM sqlite_temp_master
|
||||
)
|
||||
WHERE type = 'table'
|
||||
AND name = ?
|
||||
SQL
|
||||
,
|
||||
[$table],
|
||||
);
|
||||
|
||||
if ($sql !== false) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getForeignKeyDetails(string $table): array
|
||||
{
|
||||
$createSql = $this->getCreateTableSQL($table);
|
||||
|
||||
if (
|
||||
preg_match_all(
|
||||
'#
|
||||
(?:CONSTRAINT\s+(\S+)\s+)?
|
||||
(?:FOREIGN\s+KEY[^)]+\)\s*)?
|
||||
REFERENCES\s+\S+\s*(?:\([^)]+\))?
|
||||
(?:
|
||||
[^,]*?
|
||||
(NOT\s+DEFERRABLE|DEFERRABLE)
|
||||
(?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
|
||||
)?#isx',
|
||||
$createSql,
|
||||
$match,
|
||||
) === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names = $match[1];
|
||||
$deferrable = $match[2];
|
||||
$deferred = $match[3];
|
||||
$details = [];
|
||||
|
||||
for ($i = 0, $count = count($match[0]); $i < $count; $i++) {
|
||||
$details[] = [
|
||||
'constraint_name' => $names[$i] ?? '',
|
||||
'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0,
|
||||
'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
public function createComparator(/* ComparatorConfig $config = new ComparatorConfig() */): Comparator
|
||||
{
|
||||
return new SQLite\Comparator($this->platform, func_num_args() > 0 ? func_get_arg(0) : new ComparatorConfig());
|
||||
}
|
||||
|
||||
protected function selectTableNames(string $databaseName): Result
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT name AS table_name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')
|
||||
UNION ALL
|
||||
SELECT name
|
||||
FROM sqlite_temp_master
|
||||
WHERE type = 'table'
|
||||
ORDER BY name
|
||||
SQL;
|
||||
|
||||
return $this->connection->executeQuery($sql);
|
||||
}
|
||||
|
||||
protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT t.name AS table_name,
|
||||
c.*
|
||||
FROM sqlite_master t
|
||||
JOIN pragma_table_info(t.name) c
|
||||
WHERE %s
|
||||
ORDER BY t.name,
|
||||
c.cid
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, $params),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @link https://www.sqlite.org/pragma.html#pragma_index_info
|
||||
* @link https://www.sqlite.org/pragma.html#pragma_table_info
|
||||
* @link https://www.sqlite.org/fileformat2.html#internal_schema_objects
|
||||
*/
|
||||
protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT t.name AS table_name,
|
||||
i.name,
|
||||
i."unique",
|
||||
c.name AS column_name
|
||||
FROM sqlite_master t
|
||||
JOIN pragma_index_list(t.name) i
|
||||
JOIN pragma_index_info(i.name) c
|
||||
WHERE %s
|
||||
AND i.name NOT LIKE 'sqlite_%%'
|
||||
ORDER BY t.name, i.seq, c.seqno
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, $params),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT t.name AS table_name,
|
||||
p.*
|
||||
FROM sqlite_master t
|
||||
JOIN pragma_foreign_key_list(t.name) p
|
||||
ON p.seq != '-1'
|
||||
WHERE %s
|
||||
ORDER BY t.name,
|
||||
p.id DESC,
|
||||
p.seq
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, $params),
|
||||
);
|
||||
|
||||
return $this->connection->executeQuery($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableColumns(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$rows = parent::fetchTableColumns($databaseName, $tableName);
|
||||
|
||||
$sqlByTable = $pkColumnNamesByTable = $result = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$tableName = $row['table_name'];
|
||||
|
||||
$sqlByTable[$tableName] ??= $this->getCreateTableSQL($tableName);
|
||||
|
||||
if ($row['pk'] === 0 || $row['pk'] === '0' || $row['type'] !== 'INTEGER') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pkColumnNamesByTable[$tableName][] = $row['name'];
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$tableName = $row['table_name'];
|
||||
$columnName = $row['name'];
|
||||
$tableSQL = $sqlByTable[$row['table_name']];
|
||||
|
||||
$result[] = array_merge($row, [
|
||||
'autoincrement' => isset($pkColumnNamesByTable[$tableName])
|
||||
&& $pkColumnNamesByTable[$tableName] === [$columnName],
|
||||
'collation' => $this->parseColumnCollationFromSQL($columnName, $tableSQL),
|
||||
'comment' => $this->parseColumnCommentFromSQL($columnName, $tableSQL),
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchIndexColumns(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$pkColumnNameRows = $this->fetchPrimaryKeyColumns($tableName);
|
||||
|
||||
foreach ($pkColumnNameRows as $pkColumnNameRow) {
|
||||
$result[] = [
|
||||
'table_name' => $pkColumnNameRow['table_name'],
|
||||
'key_name' => 'primary',
|
||||
'primary' => true,
|
||||
'non_unique' => false,
|
||||
'column_name' => $pkColumnNameRow['name'],
|
||||
];
|
||||
}
|
||||
|
||||
$indexColumnRows = parent::fetchIndexColumns($databaseName, $tableName);
|
||||
|
||||
foreach ($indexColumnRows as $indexColumnRow) {
|
||||
$result[] = [
|
||||
'table_name' => $indexColumnRow['table_name'],
|
||||
'key_name' => $indexColumnRow['name'],
|
||||
'primary' => false,
|
||||
'non_unique' => ! $indexColumnRow['unique'],
|
||||
'column_name' => $indexColumnRow['column_name'],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches names of primary key columns. If the table name is specified, narrows down the selection to this table.
|
||||
*
|
||||
* @link https://www.sqlite.org/pragma.html#pragma_table_info
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function fetchPrimaryKeyColumns(?string $tableName = null): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
$sql = sprintf(
|
||||
<<<'SQL'
|
||||
SELECT t.name AS table_name,
|
||||
p.name
|
||||
FROM sqlite_master t
|
||||
JOIN pragma_table_info(t.name) p
|
||||
WHERE %s
|
||||
AND p.pk > 0
|
||||
ORDER BY t.name,
|
||||
p.pk
|
||||
SQL,
|
||||
$this->getWhereClause($tableName, $params),
|
||||
);
|
||||
|
||||
return $this->connection->fetchAllAssociative($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchForeignKeyColumns(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
$columnsByTable = [];
|
||||
foreach (parent::fetchForeignKeyColumns($databaseName, $tableName) as $column) {
|
||||
$columnsByTable[$column['table_name']][] = $column;
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
foreach ($columnsByTable as $table => $tableColumns) {
|
||||
$foreignKeyDetails = $this->getForeignKeyDetails($table);
|
||||
$foreignKeyCount = count($foreignKeyDetails);
|
||||
|
||||
foreach ($tableColumns as $column) {
|
||||
// SQLite identifies foreign keys in reverse order of appearance in SQL
|
||||
$columns[] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
|
||||
{
|
||||
if ($tableName === null) {
|
||||
$tables = $this->listTableNames();
|
||||
} else {
|
||||
$tables = [$tableName];
|
||||
}
|
||||
|
||||
$tableOptions = [];
|
||||
foreach ($tables as $table) {
|
||||
$comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table));
|
||||
|
||||
if ($comment === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableOptions[$table]['comment'] = $comment;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
/** @param list<string> $params */
|
||||
private function getWhereClause(?string $tableName, array &$params): string
|
||||
{
|
||||
$conditions = [
|
||||
"t.type = 'table'",
|
||||
"t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
|
||||
];
|
||||
|
||||
if ($tableName !== null) {
|
||||
$conditions[] = 't.name = ?';
|
||||
$params[] = $tableName;
|
||||
}
|
||||
|
||||
return implode(' AND ', $conditions);
|
||||
}
|
||||
}
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\NamespaceAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Exception\SequenceAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Exception\SequenceDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Exception\TableAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Exception\TableDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\SQL\Builder\CreateSchemaObjectsSQLBuilder;
|
||||
use Doctrine\DBAL\SQL\Builder\DropSchemaObjectsSQLBuilder;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Object representation of a database schema.
|
||||
*
|
||||
* Different vendors have very inconsistent naming with regard to the concept
|
||||
* of a "schema". Doctrine understands a schema as the entity that conceptually
|
||||
* wraps a set of database objects such as tables, sequences, indexes and
|
||||
* foreign keys that belong to each other into a namespace. A Doctrine Schema
|
||||
* has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more
|
||||
* related to the concept of "DATABASE" that exists in MySQL and PostgreSQL.
|
||||
*
|
||||
* Every asset in the doctrine schema has a name. A name consists of either a
|
||||
* namespace.local name pair or just a local unqualified name.
|
||||
*
|
||||
* Objects in a schema can be referenced by unqualified names or qualified
|
||||
* names but not both. Whether a given schema uses qualified or unqualified
|
||||
* names is determined at runtime by the presence of objects with unqualified
|
||||
* names and namespaces.
|
||||
*
|
||||
* The abstraction layer that covers a PostgreSQL schema is the namespace of a
|
||||
* database object (asset). A schema can have a name, which will be used as
|
||||
* default namespace for the unqualified database objects that are created in
|
||||
* the schema. If a schema uses qualified names and has a name, unqualified
|
||||
* names will be resolved against the corresponding namespace.
|
||||
*
|
||||
* In the case of MySQL where cross-database queries are allowed this leads to
|
||||
* databases being "misinterpreted" as namespaces. This is intentional, however
|
||||
* the CREATE/DROP SQL visitors will just filter this queries and do not
|
||||
* execute them. Only the queries for the currently connected database are
|
||||
* executed.
|
||||
*
|
||||
* @extends AbstractAsset<UnqualifiedName>
|
||||
*/
|
||||
class Schema extends AbstractAsset
|
||||
{
|
||||
/**
|
||||
* The namespaces in this schema.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $namespaces = [];
|
||||
|
||||
/** @var array<string, Table> */
|
||||
protected array $_tables = [];
|
||||
|
||||
/** @var array<string, Sequence> */
|
||||
protected array $_sequences = [];
|
||||
|
||||
protected SchemaConfig $_schemaConfig;
|
||||
|
||||
/**
|
||||
* Indicates whether the schema uses unqualified names for its objects. Once this flag is set to true, it won't be
|
||||
* unset even after the objects with unqualified names have been dropped from the schema.
|
||||
*/
|
||||
private bool $usesUnqualifiedNames = false;
|
||||
|
||||
/**
|
||||
* @param array<Table> $tables
|
||||
* @param array<Sequence> $sequences
|
||||
* @param array<string> $namespaces
|
||||
*/
|
||||
public function __construct(
|
||||
array $tables = [],
|
||||
array $sequences = [],
|
||||
?SchemaConfig $schemaConfig = null,
|
||||
array $namespaces = [],
|
||||
) {
|
||||
$schemaConfig ??= new SchemaConfig();
|
||||
|
||||
$this->_schemaConfig = $schemaConfig;
|
||||
|
||||
$name = $schemaConfig->getName();
|
||||
|
||||
parent::__construct($name ?? '');
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
$this->createNamespace($namespace);
|
||||
}
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$table->setSchemaConfig($this->_schemaConfig);
|
||||
$this->_addTable($table);
|
||||
}
|
||||
|
||||
foreach ($sequences as $sequence) {
|
||||
$this->_addSequence($sequence);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public function getName(): string
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6734',
|
||||
'Using Schema as AbstractAsset, including %s, is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
protected function getNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return Parsers::getUnqualifiedNameParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* The object representation of the name isn't used because {@see Schema} is not an {@see AbstractAsset}.
|
||||
*
|
||||
* This method implements the abstract method in the parent class and will be removed once {@see Schema} stops
|
||||
* extending {@see AbstractAsset}.
|
||||
*/
|
||||
protected function setName(?Name $name): void
|
||||
{
|
||||
}
|
||||
|
||||
protected function _addTable(Table $table): void
|
||||
{
|
||||
$resolvedName = $this->resolveName($table);
|
||||
|
||||
$key = $this->getKeyFromResolvedName($resolvedName);
|
||||
|
||||
if (isset($this->_tables[$key])) {
|
||||
throw TableAlreadyExists::new($resolvedName->getName());
|
||||
}
|
||||
|
||||
$namespaceName = $resolvedName->getNamespaceName();
|
||||
|
||||
if ($namespaceName !== null) {
|
||||
if (
|
||||
! $table->isInDefaultNamespace($this->getName())
|
||||
&& ! $this->hasNamespace($namespaceName)
|
||||
) {
|
||||
$this->createNamespace($namespaceName);
|
||||
}
|
||||
} else {
|
||||
$this->usesUnqualifiedNames = true;
|
||||
}
|
||||
|
||||
$this->_tables[$key] = $table;
|
||||
}
|
||||
|
||||
protected function _addSequence(Sequence $sequence): void
|
||||
{
|
||||
$resolvedName = $this->resolveName($sequence);
|
||||
|
||||
$key = $this->getKeyFromResolvedName($resolvedName);
|
||||
|
||||
if (isset($this->_sequences[$key])) {
|
||||
throw SequenceAlreadyExists::new($resolvedName->getName());
|
||||
}
|
||||
|
||||
$namespaceName = $resolvedName->getNamespaceName();
|
||||
|
||||
if ($namespaceName !== null) {
|
||||
if (
|
||||
! $sequence->isInDefaultNamespace($this->getName())
|
||||
&& ! $this->hasNamespace($namespaceName)
|
||||
) {
|
||||
$this->createNamespace($namespaceName);
|
||||
}
|
||||
} else {
|
||||
$this->usesUnqualifiedNames = true;
|
||||
}
|
||||
|
||||
$this->_sequences[$key] = $sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespaces of this schema.
|
||||
*
|
||||
* @return list<string> A list of namespace names.
|
||||
*/
|
||||
public function getNamespaces(): array
|
||||
{
|
||||
return array_values($this->namespaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all tables of this schema.
|
||||
*
|
||||
* @return list<Table>
|
||||
*/
|
||||
public function getTables(): array
|
||||
{
|
||||
return array_values($this->_tables);
|
||||
}
|
||||
|
||||
public function getTable(string $name): Table
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
if (! isset($this->_tables[$key])) {
|
||||
throw TableDoesNotExist::new($name);
|
||||
}
|
||||
|
||||
return $this->_tables[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key that will be used to store the given object in a collection of such objects based on its name.
|
||||
*
|
||||
* If the schema uses unqualified names, the object name must be unqualified. If the schema uses qualified names,
|
||||
* the object name must be qualified.
|
||||
*
|
||||
* The resulting key is the lower-cased full object name. Lower-casing is
|
||||
* actually wrong, but we have to do it to keep our sanity. If you are
|
||||
* using database objects that only differentiate in the casing (FOO vs
|
||||
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
|
||||
*
|
||||
* @param AbstractAsset<N> $asset
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
private function getKeyFromResolvedName(AbstractAsset $asset): string
|
||||
{
|
||||
$key = $asset->getName();
|
||||
|
||||
if ($asset->getNamespaceName() !== null) {
|
||||
if ($this->usesUnqualifiedNames) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6677#user-content-qualified-names',
|
||||
'Using qualified names to create or reference objects in a schema that uses unqualified '
|
||||
. 'names is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
$key = $this->getName() . '.' . $key;
|
||||
} elseif (count($this->namespaces) > 0) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6677#user-content-unqualified-names',
|
||||
'Using unqualified names to create or reference objects in a schema that uses qualified '
|
||||
. 'names and lacks a default namespace configuration is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
return strtolower($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key that will be used to store the given object with the given name in a collection of such objects.
|
||||
*
|
||||
* If the schema configuration has the default namespace, an unqualified name will be resolved to qualified against
|
||||
* that namespace.
|
||||
*/
|
||||
private function getKeyFromName(string $name): string
|
||||
{
|
||||
return $this->getKeyFromResolvedName($this->resolveName(new Identifier($name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the qualified or unqualified name against the current schema name and returns a qualified name.
|
||||
*
|
||||
* @param AbstractAsset<N> $asset A database object with optionally qualified name.
|
||||
*
|
||||
* @template N of Name
|
||||
*/
|
||||
private function resolveName(AbstractAsset $asset): AbstractAsset
|
||||
{
|
||||
if ($asset->getNamespaceName() === null) {
|
||||
$defaultNamespaceName = $this->getName();
|
||||
|
||||
if ($defaultNamespaceName !== '') {
|
||||
return new Identifier($defaultNamespaceName . '.' . $asset->getName());
|
||||
}
|
||||
}
|
||||
|
||||
return $asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unquoted representation of a given asset name.
|
||||
*/
|
||||
private function getUnquotedAssetName(string $assetName): string
|
||||
{
|
||||
if ($this->isIdentifierQuoted($assetName)) {
|
||||
return $this->trimQuotes($assetName);
|
||||
}
|
||||
|
||||
return $assetName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this schema have a namespace with the given name?
|
||||
*/
|
||||
public function hasNamespace(string $name): bool
|
||||
{
|
||||
$name = strtolower($this->getUnquotedAssetName($name));
|
||||
|
||||
return isset($this->namespaces[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this schema have a table with the given name?
|
||||
*/
|
||||
public function hasTable(string $name): bool
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
|
||||
return isset($this->_tables[$key]);
|
||||
}
|
||||
|
||||
public function hasSequence(string $name): bool
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
|
||||
return isset($this->_sequences[$key]);
|
||||
}
|
||||
|
||||
public function getSequence(string $name): Sequence
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
if (! isset($this->_sequences[$key])) {
|
||||
throw SequenceDoesNotExist::new($name);
|
||||
}
|
||||
|
||||
return $this->_sequences[$key];
|
||||
}
|
||||
|
||||
/** @return list<Sequence> */
|
||||
public function getSequences(): array
|
||||
{
|
||||
return array_values($this->_sequences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new namespace.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function createNamespace(string $name): self
|
||||
{
|
||||
$unquotedName = strtolower($this->getUnquotedAssetName($name));
|
||||
|
||||
if (isset($this->namespaces[$unquotedName])) {
|
||||
throw NamespaceAlreadyExists::new($unquotedName);
|
||||
}
|
||||
|
||||
$this->namespaces[$unquotedName] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new table.
|
||||
*/
|
||||
public function createTable(string $name): Table
|
||||
{
|
||||
$table = new Table($name, [], [], [], [], [], $this->_schemaConfig->toTableConfiguration());
|
||||
$this->_addTable($table);
|
||||
|
||||
foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) {
|
||||
$table->addOption($option, $value);
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a table.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function renameTable(string $oldName, string $newName): self
|
||||
{
|
||||
$table = $this->getTable($oldName);
|
||||
|
||||
$identifier = new Identifier($newName);
|
||||
|
||||
$table->_name = $identifier->_name;
|
||||
$table->_namespace = $identifier->_namespace;
|
||||
$table->_quoted = $identifier->_quoted;
|
||||
|
||||
$this->dropTable($oldName);
|
||||
$this->_addTable($table);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table from the schema.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function dropTable(string $name): self
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
if (! isset($this->_tables[$key])) {
|
||||
throw TableDoesNotExist::new($name);
|
||||
}
|
||||
|
||||
unset($this->_tables[$key]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sequence.
|
||||
*/
|
||||
public function createSequence(string $name, int $allocationSize = 1, int $initialValue = 1): Sequence
|
||||
{
|
||||
$seq = new Sequence($name, $allocationSize, $initialValue);
|
||||
$this->_addSequence($seq);
|
||||
|
||||
return $seq;
|
||||
}
|
||||
|
||||
/** @return $this */
|
||||
public function dropSequence(string $name): self
|
||||
{
|
||||
$key = $this->getKeyFromName($name);
|
||||
unset($this->_sequences[$key]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of necessary SQL queries to create the schema on the given platform.
|
||||
*
|
||||
* @return list<string>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function toSql(AbstractPlatform $platform): array
|
||||
{
|
||||
$builder = new CreateSchemaObjectsSQLBuilder($platform);
|
||||
|
||||
return $builder->buildSQL($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of necessary SQL queries to drop the schema on the given platform.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function toDropSql(AbstractPlatform $platform): array
|
||||
{
|
||||
$builder = new DropSchemaObjectsSQLBuilder($platform);
|
||||
|
||||
return $builder->buildSQL($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloning a Schema triggers a deep clone of all related assets.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->_tables as $k => $table) {
|
||||
$this->_tables[$k] = clone $table;
|
||||
}
|
||||
|
||||
foreach ($this->_sequences as $k => $sequence) {
|
||||
$this->_sequences[$k] = clone $sequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
/**
|
||||
* Configuration for a Schema.
|
||||
*/
|
||||
class SchemaConfig
|
||||
{
|
||||
/** @var positive-int */
|
||||
protected int $maxIdentifierLength = 63;
|
||||
|
||||
/** @var ?non-empty-string */
|
||||
protected ?string $name = null;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
protected array $defaultTableOptions = [];
|
||||
|
||||
/** @param positive-int $length */
|
||||
public function setMaxIdentifierLength(int $length): void
|
||||
{
|
||||
$this->maxIdentifierLength = $length;
|
||||
}
|
||||
|
||||
/** @return positive-int */
|
||||
public function getMaxIdentifierLength(): int
|
||||
{
|
||||
return $this->maxIdentifierLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default namespace of schema objects.
|
||||
*
|
||||
* @return ?non-empty-string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default namespace name of schema objects.
|
||||
*
|
||||
* @param ?non-empty-string $name
|
||||
*/
|
||||
public function setName(?string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default options that are passed to Table instances created with
|
||||
* Schema#createTable().
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getDefaultTableOptions(): array
|
||||
{
|
||||
return $this->defaultTableOptions;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $defaultTableOptions */
|
||||
public function setDefaultTableOptions(array $defaultTableOptions): void
|
||||
{
|
||||
$this->defaultTableOptions = $defaultTableOptions;
|
||||
}
|
||||
|
||||
public function toTableConfiguration(): TableConfiguration
|
||||
{
|
||||
return new TableConfiguration($this->maxIdentifierLength);
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use function array_filter;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Differences between two schemas.
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class SchemaDiff
|
||||
{
|
||||
/** @var array<TableDiff> */
|
||||
private readonly array $alteredTables;
|
||||
|
||||
/**
|
||||
* 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<Table> $createdTables
|
||||
* @param array<TableDiff> $alteredTables
|
||||
* @param array<Table> $droppedTables
|
||||
* @param array<Sequence> $createdSequences
|
||||
* @param array<Sequence> $alteredSequences
|
||||
* @param array<Sequence> $droppedSequences
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $createdSchemas,
|
||||
private readonly array $droppedSchemas,
|
||||
private readonly array $createdTables,
|
||||
array $alteredTables,
|
||||
private readonly array $droppedTables,
|
||||
private readonly array $createdSequences,
|
||||
private readonly array $alteredSequences,
|
||||
private readonly array $droppedSequences,
|
||||
) {
|
||||
$this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool {
|
||||
return ! $diff->isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getCreatedSchemas(): array
|
||||
{
|
||||
return $this->createdSchemas;
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getDroppedSchemas(): array
|
||||
{
|
||||
return $this->droppedSchemas;
|
||||
}
|
||||
|
||||
/** @return array<Table> */
|
||||
public function getCreatedTables(): array
|
||||
{
|
||||
return $this->createdTables;
|
||||
}
|
||||
|
||||
/** @return array<TableDiff> */
|
||||
public function getAlteredTables(): array
|
||||
{
|
||||
return $this->alteredTables;
|
||||
}
|
||||
|
||||
/** @return array<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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
|
||||
interface SchemaException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
/**
|
||||
* Creates a schema manager for the given connection.
|
||||
*
|
||||
* This interface is an extension point for applications that need to override schema managers.
|
||||
*/
|
||||
interface SchemaManagerFactory
|
||||
{
|
||||
public function createSchemaManager(Connection $connection): AbstractSchemaManager;
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\OptionallyQualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function count;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Sequence structure.
|
||||
*
|
||||
* @extends AbstractNamedObject<OptionallyQualifiedName>
|
||||
*/
|
||||
class Sequence extends AbstractNamedObject
|
||||
{
|
||||
protected int $allocationSize = 1;
|
||||
|
||||
protected int $initialValue = 1;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
int $allocationSize = 1,
|
||||
int $initialValue = 1,
|
||||
protected ?int $cache = null,
|
||||
) {
|
||||
parent::__construct($name);
|
||||
|
||||
$this->setAllocationSize($allocationSize);
|
||||
$this->setInitialValue($initialValue);
|
||||
}
|
||||
|
||||
protected function getNameParser(): OptionallyQualifiedNameParser
|
||||
{
|
||||
return Parsers::getOptionallyQualifiedNameParser();
|
||||
}
|
||||
|
||||
public function getAllocationSize(): int
|
||||
{
|
||||
return $this->allocationSize;
|
||||
}
|
||||
|
||||
public function getInitialValue(): int
|
||||
{
|
||||
return $this->initialValue;
|
||||
}
|
||||
|
||||
public function getCache(): ?int
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function setAllocationSize(int $allocationSize): self
|
||||
{
|
||||
$this->allocationSize = $allocationSize;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setInitialValue(int $initialValue): self
|
||||
{
|
||||
$this->initialValue = $initialValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCache(int $cache): self
|
||||
{
|
||||
$this->cache = $cache;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this sequence is an autoincrement sequence for a given table.
|
||||
*
|
||||
* This is used inside the comparator to not report sequences as missing,
|
||||
* when the "from" schema implicitly creates the sequences.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function isAutoIncrementsFor(Table $table): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6654',
|
||||
'%s is deprecated and will be removed in 5.0.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$primaryKey = $table->getPrimaryKey();
|
||||
|
||||
if ($primaryKey === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pkColumns = $primaryKey->getColumns();
|
||||
|
||||
if (count($pkColumns) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$column = $table->getColumn($pkColumns[0]);
|
||||
|
||||
if (! $column->getAutoincrement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sequenceName = $this->getShortestName($table->getNamespaceName());
|
||||
$tableName = $table->getShortestName($table->getNamespaceName());
|
||||
$tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName()));
|
||||
|
||||
return $tableSequenceName === $sequenceName;
|
||||
}
|
||||
}
|
||||
+1201
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
/**
|
||||
* Contains platform-specific parameters used for creating and managing objects scoped to a {@see Table}.
|
||||
*/
|
||||
final readonly class TableConfiguration
|
||||
{
|
||||
/**
|
||||
* @internal The configuration can be only instantiated by a {@see SchemaConfig}.
|
||||
*
|
||||
* @param positive-int $maxIdentifierLength
|
||||
*/
|
||||
public function __construct(private int $maxIdentifierLength)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum length of identifiers to be generated for the objects scoped to the table.
|
||||
*
|
||||
* @return positive-int
|
||||
*/
|
||||
public function getMaxIdentifierLength(): int
|
||||
{
|
||||
return $this->maxIdentifierLength;
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Table Diff.
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class TableDiff
|
||||
{
|
||||
/**
|
||||
* Constructs a TableDiff object.
|
||||
*
|
||||
* @internal The diff can be only instantiated by a {@see Comparator}.
|
||||
*
|
||||
* @param array<ForeignKeyConstraint> $droppedForeignKeys
|
||||
* @param array<Column> $addedColumns
|
||||
* @param array<string, ColumnDiff> $changedColumns
|
||||
* @param array<Column> $droppedColumns
|
||||
* @param array<Index> $addedIndexes
|
||||
* @param array<Index> $modifiedIndexes
|
||||
* @param array<Index> $droppedIndexes
|
||||
* @param array<string, Index> $renamedIndexes
|
||||
* @param array<ForeignKeyConstraint> $addedForeignKeys
|
||||
* @param array<ForeignKeyConstraint> $modifiedForeignKeys
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly Table $oldTable,
|
||||
private readonly array $addedColumns = [],
|
||||
private readonly array $changedColumns = [],
|
||||
private readonly array $droppedColumns = [],
|
||||
private array $addedIndexes = [],
|
||||
private readonly array $modifiedIndexes = [],
|
||||
private array $droppedIndexes = [],
|
||||
private readonly array $renamedIndexes = [],
|
||||
private readonly array $addedForeignKeys = [],
|
||||
private readonly array $modifiedForeignKeys = [],
|
||||
private readonly array $droppedForeignKeys = [],
|
||||
) {
|
||||
if (count($this->modifiedIndexes) !== 0) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6831',
|
||||
'Passing a non-empty $modifiedIndexes value to %s() is deprecated. Instead, pass dropped'
|
||||
. ' indexes via $droppedIndexes and added indexes via $addedIndexes.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
if (count($modifiedForeignKeys) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6827',
|
||||
'Passing a non-empty $modifiedForeignKeys value to %s() is deprecated. Instead, pass dropped'
|
||||
. ' constraints via $droppedForeignKeys and added constraints via $addedForeignKeys.',
|
||||
__METHOD__,
|
||||
);
|
||||
}
|
||||
|
||||
public function getOldTable(): Table
|
||||
{
|
||||
return $this->oldTable;
|
||||
}
|
||||
|
||||
/** @return array<Column> */
|
||||
public function getAddedColumns(): array
|
||||
{
|
||||
return $this->addedColumns;
|
||||
}
|
||||
|
||||
/** @return array<string, ColumnDiff> */
|
||||
public function getChangedColumns(): array
|
||||
{
|
||||
return $this->changedColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getChangedColumns()} instead.
|
||||
*
|
||||
* @return list<ColumnDiff>
|
||||
*/
|
||||
public function getModifiedColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6280',
|
||||
'%s is deprecated, use `getChangedColumns()` instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_values(array_filter(
|
||||
$this->getChangedColumns(),
|
||||
static fn (ColumnDiff $diff): bool => $diff->countChangedProperties() > ($diff->hasNameChanged() ? 1 : 0),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getChangedColumns()} instead.
|
||||
*
|
||||
* @return array<string,Column>
|
||||
*/
|
||||
public function getRenamedColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6280',
|
||||
'%s is deprecated, you should use `getChangedColumns()` instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
$renamed = [];
|
||||
foreach ($this->getChangedColumns() as $diff) {
|
||||
if (! $diff->hasNameChanged()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldColumnName = $diff->getOldColumn()->getName();
|
||||
$renamed[$oldColumnName] = $diff->getNewColumn();
|
||||
}
|
||||
|
||||
return $renamed;
|
||||
}
|
||||
|
||||
/** @return array<Column> */
|
||||
public function getDroppedColumns(): array
|
||||
{
|
||||
return $this->droppedColumns;
|
||||
}
|
||||
|
||||
/** @return array<Index> */
|
||||
public function getAddedIndexes(): array
|
||||
{
|
||||
return $this->addedIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getAddedIndexes()} and {@see getDroppedIndexes()} instead.
|
||||
*
|
||||
* @return array<Index>
|
||||
*/
|
||||
public function getModifiedIndexes(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6831',
|
||||
'%s() is deprecated, use getAddedIndexes() and getDroppedIndexes() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->modifiedIndexes;
|
||||
}
|
||||
|
||||
/** @return array<Index> */
|
||||
public function getDroppedIndexes(): array
|
||||
{
|
||||
return $this->droppedIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** @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
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6827',
|
||||
'%s() is deprecated, use getDroppedForeignKeys() and getAddedForeignKeys() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->modifiedForeignKeys;
|
||||
}
|
||||
|
||||
/** @return array<ForeignKeyConstraint> */
|
||||
public function getDroppedForeignKeys(): array
|
||||
{
|
||||
return $this->droppedForeignKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the diff is empty (contains no changes).
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return count($this->addedColumns) === 0
|
||||
&& count($this->changedColumns) === 0
|
||||
&& count($this->droppedColumns) === 0
|
||||
&& count($this->addedIndexes) === 0
|
||||
&& count($this->modifiedIndexes) === 0
|
||||
&& count($this->droppedIndexes) === 0
|
||||
&& count($this->renamedIndexes) === 0
|
||||
&& count($this->addedForeignKeys) === 0
|
||||
&& count($this->modifiedForeignKeys) === 0
|
||||
&& count($this->droppedForeignKeys) === 0;
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectAlreadyExists;
|
||||
use Doctrine\DBAL\Schema\Collections\Exception\ObjectDoesNotExist;
|
||||
use Doctrine\DBAL\Schema\Collections\OptionallyUnqualifiedNamedObjectSet;
|
||||
use Doctrine\DBAL\Schema\Collections\UnqualifiedNamedObjectSet;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidTableDefinition;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidTableModification;
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function strcasecmp;
|
||||
|
||||
final class TableEditor
|
||||
{
|
||||
private ?OptionallyQualifiedName $name = null;
|
||||
|
||||
/** @var UnqualifiedNamedObjectSet<Column> */
|
||||
private readonly UnqualifiedNamedObjectSet $columns;
|
||||
|
||||
/** @var UnqualifiedNamedObjectSet<Index> */
|
||||
private UnqualifiedNamedObjectSet $indexes;
|
||||
|
||||
private ?PrimaryKeyConstraint $primaryKeyConstraint = null;
|
||||
|
||||
/** @var OptionallyUnqualifiedNamedObjectSet<UniqueConstraint> */
|
||||
private readonly OptionallyUnqualifiedNamedObjectSet $uniqueConstraints;
|
||||
|
||||
/** @var OptionallyUnqualifiedNamedObjectSet<ForeignKeyConstraint> */
|
||||
private readonly OptionallyUnqualifiedNamedObjectSet $foreignKeyConstraints;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $options = [];
|
||||
|
||||
private string $comment = '';
|
||||
|
||||
private ?TableConfiguration $configuration = null;
|
||||
|
||||
/** @internal Use {@link Table::editor()} or {@link Table::edit()} to create an instance */
|
||||
public function __construct()
|
||||
{
|
||||
/** @var UnqualifiedNamedObjectSet<Column> $columns */
|
||||
$columns = new UnqualifiedNamedObjectSet();
|
||||
$this->columns = $columns;
|
||||
|
||||
/** @var UnqualifiedNamedObjectSet<Index> $indexes */
|
||||
$indexes = new UnqualifiedNamedObjectSet();
|
||||
$this->indexes = $indexes;
|
||||
|
||||
/** @var OptionallyUnqualifiedNamedObjectSet<UniqueConstraint> $uniqueConstraints */
|
||||
$uniqueConstraints = new OptionallyUnqualifiedNamedObjectSet();
|
||||
$this->uniqueConstraints = $uniqueConstraints;
|
||||
|
||||
/** @var OptionallyUnqualifiedNamedObjectSet<ForeignKeyConstraint> $foreignKeyConstraints */
|
||||
$foreignKeyConstraints = new OptionallyUnqualifiedNamedObjectSet();
|
||||
$this->foreignKeyConstraints = $foreignKeyConstraints;
|
||||
}
|
||||
|
||||
public function setName(OptionallyQualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $unqualifiedName
|
||||
* @param ?non-empty-string $qualifier
|
||||
*/
|
||||
public function setUnquotedName(string $unqualifiedName, ?string $qualifier = null): self
|
||||
{
|
||||
$this->name = OptionallyQualifiedName::unquoted($unqualifiedName, $qualifier);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $unqualifiedName
|
||||
* @param ?non-empty-string $qualifier
|
||||
*/
|
||||
public function setQuotedName(string $unqualifiedName, ?string $qualifier = null): self
|
||||
{
|
||||
$this->name = OptionallyQualifiedName::quoted($unqualifiedName, $qualifier);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumns(Column $firstColumn, Column ...$otherColumns): self
|
||||
{
|
||||
$this->columns->clear();
|
||||
|
||||
foreach ([$firstColumn, ...$otherColumns] as $column) {
|
||||
$this->addColumn($column);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addColumn(Column $column): self
|
||||
{
|
||||
try {
|
||||
$this->columns->add($column);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::columnAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param callable(ColumnEditor): void $modification */
|
||||
public function modifyColumn(UnqualifiedName $columnName, callable $modification): self
|
||||
{
|
||||
try {
|
||||
$this->columns->modify($columnName, static function (Column $column) use ($modification): Column {
|
||||
$editor = $column->edit();
|
||||
|
||||
$modification($editor);
|
||||
|
||||
return $editor->create();
|
||||
});
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::columnDoesNotExist($this->name, $e);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::columnAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $columnName
|
||||
* @param callable(ColumnEditor): void $modification
|
||||
*/
|
||||
public function modifyColumnByUnquotedName(string $columnName, callable $modification): self
|
||||
{
|
||||
return $this->modifyColumn(UnqualifiedName::unquoted($columnName), $modification);
|
||||
}
|
||||
|
||||
public function renameColumn(UnqualifiedName $oldColumnName, UnqualifiedName $newColumnName): self
|
||||
{
|
||||
$this->modifyColumn($oldColumnName, static function (ColumnEditor $editor) use ($newColumnName): void {
|
||||
$editor->setName($newColumnName);
|
||||
});
|
||||
|
||||
$this->renameColumnInIndexes($oldColumnName, $newColumnName);
|
||||
$this->renameColumnInPrimaryKeyConstraint($oldColumnName, $newColumnName);
|
||||
$this->renameColumnInForeignKeyConstraints($oldColumnName, $newColumnName);
|
||||
$this->renameColumnInUniqueConstraints($oldColumnName, $newColumnName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function renameColumnInIndexes(UnqualifiedName $oldColumnName, UnqualifiedName $newColumnName): void
|
||||
{
|
||||
foreach ($this->indexes->toList() as $index) {
|
||||
$modified = false;
|
||||
$columns = [];
|
||||
|
||||
foreach ($index->getIndexedColumns() as $column) {
|
||||
$columnName = $column->getColumnName();
|
||||
if ($this->namesEqual($columnName, $oldColumnName)) {
|
||||
$columns[] = new Index\IndexedColumn($newColumnName, $column->getLength());
|
||||
$modified = true;
|
||||
} else {
|
||||
$columns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $modified) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->indexes->modify($index->getObjectName(), static function (Index $index) use ($columns): Index {
|
||||
return $index->edit()
|
||||
->setColumns(...$columns)
|
||||
->create();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function renameColumnInPrimaryKeyConstraint(
|
||||
UnqualifiedName $oldColumnName,
|
||||
UnqualifiedName $newColumnName,
|
||||
): void {
|
||||
if ($this->primaryKeyConstraint === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modified = false;
|
||||
$columnNames = [];
|
||||
|
||||
foreach ($this->primaryKeyConstraint->getColumnNames() as $columnName) {
|
||||
if ($this->namesEqual($columnName, $oldColumnName)) {
|
||||
$columnNames[] = $newColumnName;
|
||||
$modified = true;
|
||||
} else {
|
||||
$columnNames[] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $modified) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->primaryKeyConstraint = $this->primaryKeyConstraint->edit()
|
||||
->setColumnNames(...$columnNames)
|
||||
->create();
|
||||
}
|
||||
|
||||
private function renameColumnInUniqueConstraints(
|
||||
UnqualifiedName $oldColumnName,
|
||||
UnqualifiedName $newColumnName,
|
||||
): void {
|
||||
$this->renameColumnInConstraints(
|
||||
$this->uniqueConstraints,
|
||||
$oldColumnName,
|
||||
$newColumnName,
|
||||
static fn (UniqueConstraint $constraint): array => $constraint->getColumnNames(),
|
||||
static function (UniqueConstraint $constraint, array $columnNames): UniqueConstraint {
|
||||
return $constraint->edit()
|
||||
->setColumnNames(...$columnNames)
|
||||
->create();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private function renameColumnInForeignKeyConstraints(
|
||||
UnqualifiedName $oldColumnName,
|
||||
UnqualifiedName $newColumnName,
|
||||
): void {
|
||||
$this->renameColumnInConstraints(
|
||||
$this->foreignKeyConstraints,
|
||||
$oldColumnName,
|
||||
$newColumnName,
|
||||
static fn (ForeignKeyConstraint $constraint): array => $constraint->getReferencingColumnNames(),
|
||||
static function (ForeignKeyConstraint $constraint, array $columnNames): ForeignKeyConstraint {
|
||||
return $constraint->edit()
|
||||
->setReferencingColumnNames(...$columnNames)
|
||||
->create();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to rename a column in constraints
|
||||
*
|
||||
* @param OptionallyUnqualifiedNamedObjectSet<T> $collection
|
||||
* @param callable(T): list<UnqualifiedName> $getColumnNames
|
||||
* @param callable(T, list<UnqualifiedName>): T $modify
|
||||
*
|
||||
* @template T of OptionallyNamedObject<UnqualifiedName>
|
||||
*/
|
||||
private function renameColumnInConstraints(
|
||||
OptionallyUnqualifiedNamedObjectSet $collection,
|
||||
UnqualifiedName $oldColumnName,
|
||||
UnqualifiedName $newColumnName,
|
||||
callable $getColumnNames,
|
||||
callable $modify,
|
||||
): void {
|
||||
$constraints = [];
|
||||
$anyModified = false;
|
||||
|
||||
foreach ($collection->toList() as $constraint) {
|
||||
$newColumnNames = [];
|
||||
$modified = false;
|
||||
|
||||
foreach ($getColumnNames($constraint) as $columnName) {
|
||||
if ($this->namesEqual($columnName, $oldColumnName)) {
|
||||
$newColumnNames[] = $newColumnName;
|
||||
$modified = true;
|
||||
} else {
|
||||
$newColumnNames[] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
if ($modified) {
|
||||
$constraint = $modify($constraint, $newColumnNames);
|
||||
$anyModified = true;
|
||||
}
|
||||
|
||||
$constraints[] = $constraint;
|
||||
}
|
||||
|
||||
if (! $anyModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
$collection->clear();
|
||||
|
||||
foreach ($constraints as $constraint) {
|
||||
$collection->add($constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $oldColumnName
|
||||
* @param non-empty-string $newColumnName
|
||||
*/
|
||||
public function renameColumnByUnquotedName(string $oldColumnName, string $newColumnName): self
|
||||
{
|
||||
return $this->renameColumn(
|
||||
UnqualifiedName::unquoted($oldColumnName),
|
||||
UnqualifiedName::unquoted($newColumnName),
|
||||
);
|
||||
}
|
||||
|
||||
public function dropColumn(UnqualifiedName $columnName): self
|
||||
{
|
||||
try {
|
||||
$this->columns->remove($columnName);
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::columnDoesNotExist($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $columnName */
|
||||
public function dropColumnByUnquotedName(string $columnName): self
|
||||
{
|
||||
return $this->dropColumn(UnqualifiedName::unquoted($columnName));
|
||||
}
|
||||
|
||||
public function setIndexes(Index ...$indexes): self
|
||||
{
|
||||
$this->indexes->clear();
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
$this->addIndex($index);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addIndex(Index $index): self
|
||||
{
|
||||
try {
|
||||
$this->indexes->add($index);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::indexAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function renameIndex(UnqualifiedName $oldIndexName, UnqualifiedName $newIndexName): self
|
||||
{
|
||||
try {
|
||||
$this->indexes->modify($oldIndexName, static function (Index $index) use ($newIndexName): Index {
|
||||
return $index->edit()
|
||||
->setName($newIndexName)
|
||||
->create();
|
||||
});
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::indexDoesNotExist($this->name, $e);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::indexAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $oldIndexName
|
||||
* @param non-empty-string $newIndexName
|
||||
*/
|
||||
public function renameIndexByUnquotedName(string $oldIndexName, string $newIndexName): self
|
||||
{
|
||||
return $this->renameIndex(
|
||||
UnqualifiedName::unquoted($oldIndexName),
|
||||
UnqualifiedName::unquoted($newIndexName),
|
||||
);
|
||||
}
|
||||
|
||||
public function dropIndex(UnqualifiedName $indexName): self
|
||||
{
|
||||
try {
|
||||
$this->indexes->remove($indexName);
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::indexDoesNotExist($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $indexName */
|
||||
public function dropIndexByUnquotedName(string $indexName): self
|
||||
{
|
||||
return $this->dropIndex(UnqualifiedName::unquoted($indexName));
|
||||
}
|
||||
|
||||
public function setPrimaryKeyConstraint(?PrimaryKeyConstraint $primaryKeyConstraint): self
|
||||
{
|
||||
$this->primaryKeyConstraint = $primaryKeyConstraint;
|
||||
|
||||
foreach ($this->indexes->toList() as $index) {
|
||||
if (! $index->isPrimary()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->indexes->remove($index->getObjectName());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addPrimaryKeyConstraint(PrimaryKeyConstraint $primaryKeyConstraint): self
|
||||
{
|
||||
if ($this->primaryKeyConstraint !== null) {
|
||||
throw InvalidTableModification::primaryKeyConstraintAlreadyExists($this->name);
|
||||
}
|
||||
|
||||
return $this->setPrimaryKeyConstraint($primaryKeyConstraint);
|
||||
}
|
||||
|
||||
public function dropPrimaryKeyConstraint(): self
|
||||
{
|
||||
if ($this->primaryKeyConstraint === null) {
|
||||
throw InvalidTableModification::primaryKeyConstraintDoesNotExist($this->name);
|
||||
}
|
||||
|
||||
return $this->setPrimaryKeyConstraint(null);
|
||||
}
|
||||
|
||||
public function setUniqueConstraints(UniqueConstraint ...$uniqueConstraints): self
|
||||
{
|
||||
$this->uniqueConstraints->clear();
|
||||
|
||||
foreach ($uniqueConstraints as $uniqueConstraint) {
|
||||
$this->addUniqueConstraint($uniqueConstraint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUniqueConstraint(UniqueConstraint $uniqueConstraint): self
|
||||
{
|
||||
try {
|
||||
$this->uniqueConstraints->add($uniqueConstraint);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::uniqueConstraintAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dropUniqueConstraint(UnqualifiedName $constraintName): self
|
||||
{
|
||||
try {
|
||||
$this->uniqueConstraints->remove($constraintName);
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::uniqueConstraintDoesNotExist($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $constraintName */
|
||||
public function dropUniqueConstraintByUnquotedName(string $constraintName): self
|
||||
{
|
||||
return $this->dropUniqueConstraint(UnqualifiedName::unquoted($constraintName));
|
||||
}
|
||||
|
||||
public function setForeignKeyConstraints(ForeignKeyConstraint ...$foreignKeyConstraints): self
|
||||
{
|
||||
$this->foreignKeyConstraints->clear();
|
||||
|
||||
foreach ($foreignKeyConstraints as $foreignKeyConstraint) {
|
||||
$this->addForeignKeyConstraint($foreignKeyConstraint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addForeignKeyConstraint(ForeignKeyConstraint $foreignKeyConstraint): self
|
||||
{
|
||||
try {
|
||||
$this->foreignKeyConstraints->add($foreignKeyConstraint);
|
||||
} catch (ObjectAlreadyExists $e) {
|
||||
throw InvalidTableModification::foreignKeyConstraintAlreadyExists($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dropForeignKeyConstraint(UnqualifiedName $constraintName): self
|
||||
{
|
||||
try {
|
||||
$this->foreignKeyConstraints->remove($constraintName);
|
||||
} catch (ObjectDoesNotExist $e) {
|
||||
throw InvalidTableModification::foreignKeyConstraintDoesNotExist($this->name, $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $constraintName */
|
||||
public function dropForeignKeyConstraintByUnquotedName(string $constraintName): self
|
||||
{
|
||||
return $this->dropForeignKeyConstraint(UnqualifiedName::unquoted($constraintName));
|
||||
}
|
||||
|
||||
private function namesEqual(UnqualifiedName $name1, UnqualifiedName $name2): bool
|
||||
{
|
||||
return strcasecmp($name1->getIdentifier()->getValue(), $name2->getIdentifier()->getValue()) === 0;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): self
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
public function setOptions(array $options): self
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConfiguration(TableConfiguration $configuration): self
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): Table
|
||||
{
|
||||
if ($this->name === null) {
|
||||
throw InvalidTableDefinition::nameNotSet();
|
||||
}
|
||||
|
||||
if ($this->columns->isEmpty()) {
|
||||
throw InvalidTableDefinition::columnsNotSet($this->name);
|
||||
}
|
||||
|
||||
$options = $this->options;
|
||||
|
||||
if ($this->comment !== '') {
|
||||
$options['comment'] = $this->comment;
|
||||
}
|
||||
|
||||
return new Table(
|
||||
$this->name->toString(),
|
||||
$this->columns->toList(),
|
||||
$this->indexes->toList(),
|
||||
$this->uniqueConstraints->toList(),
|
||||
$this->foreignKeyConstraints->toList(),
|
||||
$options,
|
||||
$this->configuration,
|
||||
$this->primaryKeyConstraint,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidState;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Throwable;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* Represents unique constraint definition.
|
||||
*
|
||||
* @extends AbstractOptionallyNamedObject<UnqualifiedName>
|
||||
* @final This class will be made final in DBAL 5.0.
|
||||
*/
|
||||
class UniqueConstraint extends AbstractOptionallyNamedObject
|
||||
{
|
||||
/**
|
||||
* Asset identifier instances of the column names the unique constraint is associated with.
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var array<string, Identifier>
|
||||
*/
|
||||
protected array $columns = [];
|
||||
|
||||
/**
|
||||
* Platform specific flags
|
||||
*
|
||||
* @deprecated
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
protected array $flags = [];
|
||||
|
||||
/**
|
||||
* Names of the columns covered by the unique constraint.
|
||||
*
|
||||
* @var list<UnqualifiedName>
|
||||
*/
|
||||
private array $columnNames = [];
|
||||
|
||||
private bool $failedToParseColumnNames = false;
|
||||
|
||||
/**
|
||||
* @internal Use {@link UniqueConstraint::editor()} to instantiate an editor and
|
||||
* {@link UniqueConstraintEditor::create()} to create a unique constraint.
|
||||
*
|
||||
* @param non-empty-list<string> $columns
|
||||
* @param array<string> $flags
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
array $columns,
|
||||
array $flags = [],
|
||||
private readonly array $options = [],
|
||||
) {
|
||||
if (count($columns) < 1) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'Instantiation of a unique constraint without columns is deprecated.',
|
||||
);
|
||||
}
|
||||
|
||||
if (count($options) > 0) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'Using %s options is deprecated.',
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
parent::__construct($name);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$this->addColumn($column);
|
||||
}
|
||||
|
||||
foreach ($flags as $flag) {
|
||||
$this->addFlag($flag);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getNameParser(): UnqualifiedNameParser
|
||||
{
|
||||
return Parsers::getUnqualifiedNameParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the columns the constraint is associated with.
|
||||
*
|
||||
* @return non-empty-list<UnqualifiedName>
|
||||
*/
|
||||
public function getColumnNames(): array
|
||||
{
|
||||
if ($this->failedToParseColumnNames) {
|
||||
throw InvalidState::uniqueConstraintHasInvalidColumnNames($this->getName());
|
||||
}
|
||||
|
||||
if (count($this->columnNames) < 1) {
|
||||
throw InvalidState::uniqueConstraintHasEmptyColumnNames($this->getName());
|
||||
}
|
||||
|
||||
return $this->columnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getColumnNames()} instead.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated. Use getColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
/** @phpstan-ignore return.type */
|
||||
return array_keys($this->columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getColumnNames()} and {@see UnqualifiedName::toSQL()} instead.
|
||||
*
|
||||
* But only if they were defined with one or a column name
|
||||
* is a keyword reserved by the platform.
|
||||
* Otherwise, the plain unquoted value as inserted is returned.
|
||||
*
|
||||
* @param AbstractPlatform $platform The platform to use for quotation.
|
||||
*
|
||||
* @return list<string>
|
||||
*
|
||||
* Returns the quoted representation of the column names the constraint is associated with.
|
||||
*/
|
||||
public function getQuotedColumns(AbstractPlatform $platform): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated. Use getColumnNames() and UnqualifiedName::toSQL() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$columns[] = $column->getQuotedName($platform);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see getColumnNames()} instead.
|
||||
*
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public function getUnquotedColumns(): array
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated. Use getColumnNames() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_map($this->trimQuotes(...), $this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@see isClustered()} instead.
|
||||
*
|
||||
* Returns platform specific flags for unique constraint.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated. Use isClustered() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return array_keys($this->flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds flag for a unique constraint that translates to platform specific handling.
|
||||
*
|
||||
* @deprecated Use {@see UniqueConstraintEditor::setIsClustered()} instead.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @example $uniqueConstraint->addFlag('CLUSTERED')
|
||||
*/
|
||||
public function addFlag(string $flag): self
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$this->flags[strtolower($flag)] = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this unique constraint have a specific flag?
|
||||
*
|
||||
* @deprecated Use {@see isClustered()} instead.
|
||||
*/
|
||||
public function hasFlag(string $flag): bool
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated. Use isClustered() instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($this->flags[strtolower($flag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the unique constraint is clustered.
|
||||
*/
|
||||
public function isClustered(): bool
|
||||
{
|
||||
return $this->hasFlag('clustered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a flag.
|
||||
*
|
||||
* @deprecated Use {@see UniqueConstraintEditor::setIsClustered()} instead.
|
||||
*/
|
||||
public function removeFlag(string $flag): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
unset($this->flags[strtolower($flag)]);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public function hasOption(string $name): bool
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return isset($this->options[strtolower($name)]);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public function getOption(string $name): mixed
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options[strtolower($name)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
protected function addColumn(string $column): void
|
||||
{
|
||||
Deprecation::triggerIfCalledFromOutside(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'%s is deprecated.',
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
$this->columns[$column] = new Identifier($column);
|
||||
|
||||
$parser = Parsers::getUnqualifiedNameParser();
|
||||
|
||||
try {
|
||||
$this->columnNames[] = $parser->parse($column);
|
||||
} catch (Throwable $e) {
|
||||
$this->failedToParseColumnNames = true;
|
||||
|
||||
Deprecation::trigger(
|
||||
'doctrine/dbal',
|
||||
'https://github.com/doctrine/dbal/pull/6685',
|
||||
'Unable to parse column name: %s.',
|
||||
$e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new unique constraint editor.
|
||||
*/
|
||||
public static function editor(): UniqueConstraintEditor
|
||||
{
|
||||
return new UniqueConstraintEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new unique constraint editor and initializes it with the constraint's properties.
|
||||
*/
|
||||
public function edit(): UniqueConstraintEditor
|
||||
{
|
||||
return self::editor()
|
||||
->setName($this->getObjectName())
|
||||
->setColumnNames(...$this->getColumnNames())
|
||||
->setIsClustered($this->isClustered());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Exception\InvalidUniqueConstraintDefinition;
|
||||
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
final class UniqueConstraintEditor
|
||||
{
|
||||
private ?UnqualifiedName $name = null;
|
||||
|
||||
/** @var list<UnqualifiedName> */
|
||||
private array $columnNames = [];
|
||||
|
||||
private bool $isClustered = false;
|
||||
|
||||
/** @internal Use {@link UniqueConstraint::editor()} or {@link UniqueConstraint::edit()} to create an instance */
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function setName(?UnqualifiedName $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setUnquotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::unquoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $name */
|
||||
public function setQuotedName(string $name): self
|
||||
{
|
||||
$this->name = UnqualifiedName::quoted($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setColumnNames(UnqualifiedName $firstColumnName, UnqualifiedName ...$otherColumnNames): self
|
||||
{
|
||||
$this->columnNames = array_merge([$firstColumnName], array_values($otherColumnNames));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setUnquotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::unquoted($name),
|
||||
array_merge([$firstColumnName], array_values($otherColumnNames)),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $firstColumnName
|
||||
* @param non-empty-string ...$otherColumnNames
|
||||
*/
|
||||
public function setQuotedColumnNames(
|
||||
string $firstColumnName,
|
||||
string ...$otherColumnNames,
|
||||
): self {
|
||||
$this->columnNames = array_map(
|
||||
static fn (string $name): UnqualifiedName => UnqualifiedName::quoted($name),
|
||||
array_merge([$firstColumnName], array_values($otherColumnNames)),
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setIsClustered(bool $isClustered): self
|
||||
{
|
||||
$this->isClustered = $isClustered;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(): UniqueConstraint
|
||||
{
|
||||
if (count($this->columnNames) < 1) {
|
||||
throw InvalidUniqueConstraintDefinition::columnNamesAreNotSet($this->name);
|
||||
}
|
||||
|
||||
return new UniqueConstraint(
|
||||
$this->name?->toString() ?? '',
|
||||
array_map(static fn (UnqualifiedName $columnName) => $columnName->toString(), $this->columnNames),
|
||||
$this->isClustered ? ['clustered'] : [],
|
||||
);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\DBAL\Schema;
|
||||
|
||||
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
|
||||
use Doctrine\DBAL\Schema\Name\Parser\OptionallyQualifiedNameParser;
|
||||
use Doctrine\DBAL\Schema\Name\Parsers;
|
||||
|
||||
/**
|
||||
* Representation of a Database View.
|
||||
*
|
||||
* @extends AbstractNamedObject<OptionallyQualifiedName>
|
||||
*/
|
||||
class View extends AbstractNamedObject
|
||||
{
|
||||
public function __construct(string $name, private readonly string $sql)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function getNameParser(): OptionallyQualifiedNameParser
|
||||
{
|
||||
return Parsers::getOptionallyQualifiedNameParser();
|
||||
}
|
||||
|
||||
public function getSql(): string
|
||||
{
|
||||
return $this->sql;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user