File Manager
Viewing File: Index.php
<?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());
}
}