recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+422
View File
@@ -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));
}
}
+58
View File
@@ -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
{
}
@@ -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;
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
+58
View File
@@ -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;
}
}
+374
View File
@@ -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()));
}
}
@@ -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.');
}
}
@@ -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));
}
}
@@ -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),
);
}
}
+799
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Schema\Index;
enum IndexType
{
case REGULAR;
case UNIQUE;
case FULLTEXT;
case SPATIAL;
}
+34
View File
@@ -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
View File
@@ -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,
);
}
}
+537
View File
@@ -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
View File
@@ -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;
}
+51
View File
@@ -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));
}
}
+91
View File
@@ -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
View File
@@ -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));
}
}
@@ -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),
);
}
}
@@ -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);
}
}
@@ -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]);
}
}
+43
View File
@@ -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
View File
@@ -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;
}
+489
View File
@@ -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);
}
}
+622
View File
@@ -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
View File
@@ -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;
}
}
}
+74
View File
@@ -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
View File
@@ -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;
}
}
+11
View File
@@ -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
View File
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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
View File
@@ -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
View File
@@ -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,
);
}
}
+359
View File
@@ -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
View File
@@ -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;
}
}