Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doctrine:migration:diff does not work if the table has an index with a functional part. #6153

Closed
wants to merge 10 commits into from
7 changes: 6 additions & 1 deletion src/Driver/AbstractMySQLDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Doctrine\DBAL\Platforms\MariaDb1043Platform;
use Doctrine\DBAL\Platforms\MariaDb1052Platform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL8013Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\MySQLSchemaManager;
Expand Down Expand Up @@ -69,7 +70,11 @@ public function createDatabasePlatformForVersion($version)
);
}

return new MySQL80Platform();
if (version_compare($version, '8.0.13', '<')) {
return new MySQL80Platform();
}
prohalexey marked this conversation as resolved.
Show resolved Hide resolved

return new MySQL8013Platform();
}

if (version_compare($oracleMysqlVersion, '5.7.9', '>=')) {
Expand Down
5 changes: 5 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ abstract class AbstractMySQLPlatform extends AbstractPlatform
public const LENGTH_LIMIT_BLOB = 65535;
public const LENGTH_LIMIT_MEDIUMBLOB = 16777215;

public function getColumnNameForIndexFetch(): string
{
return 'COLUMN_NAME';
}

/**
* {@inheritDoc}
*/
Expand Down
14 changes: 14 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,8 @@ public function getCustomTypeDeclarationSQL(array $column)
* declaration to be used in statements like CREATE TABLE.
*
* @deprecated
*
* @throws Exception If not supported on this platform.
*/
public function getIndexFieldDeclarationListSQL(Index $index): string
{
Expand All @@ -3235,6 +3237,10 @@ public function getIndexFieldDeclarationListSQL(Index $index): string
__METHOD__,
);

if ($index->isFunctional() && ! $this->supportsFunctionalIndex()) {
throw Exception::notSupported('Functional indexes');
}

return implode(', ', $index->getQuotedColumns($this));
}

Expand Down Expand Up @@ -4394,6 +4400,14 @@ public function supportsLimitOffset()
return true;
}

/**
* A flag that indicates whether the platform supports functional indexes.
*/
public function supportsFunctionalIndex(): bool
{
return false;
}

/**
* Maximum length of any given database identifier, like tables or column names.
*
Expand Down
21 changes: 21 additions & 0 deletions src/Platforms/MySQL8013Platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Doctrine\DBAL\Platforms;

/**
* Provides features of the MySQL since 8.0.13 database platform.
*
* Note: Should not be used with versions prior to 8.0.13.
*/
class MySQL8013Platform extends MySQL80Platform
{
public function getColumnNameForIndexFetch(): string
{
return "COALESCE(COLUMN_NAME, CONCAT('(', REPLACE(EXPRESSION, '\\\''', ''''), ')'))";
}

public function supportsFunctionalIndex(): bool
{
return true;
}
}
5 changes: 5 additions & 0 deletions src/Platforms/OraclePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -1328,4 +1328,9 @@ public function createSchemaManager(Connection $connection): OracleSchemaManager
{
return new OracleSchemaManager($connection, $this);
}

public function supportsFunctionalIndex(): bool
{
return true;
}
}
5 changes: 5 additions & 0 deletions src/Platforms/PostgreSQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -1399,4 +1399,9 @@ public function createSchemaManager(Connection $connection): PostgreSQLSchemaMan
{
return new PostgreSQLSchemaManager($connection, $this);
}

public function supportsFunctionalIndex(): bool
{
return true;
}
}
5 changes: 5 additions & 0 deletions src/Platforms/SqlitePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -1482,4 +1482,9 @@ public function createSchemaManager(Connection $connection): SqliteSchemaManager
{
return new SqliteSchemaManager($connection, $this);
}

public function supportsFunctionalIndex(): bool
{
return true;
}
}
28 changes: 25 additions & 3 deletions src/Schema/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use function array_shift;
use function count;
use function strtolower;
use function substr;

class Index extends AbstractAsset implements Constraint
{
Expand All @@ -29,6 +30,9 @@ class Index extends AbstractAsset implements Constraint
/** @var bool */
protected $_isPrimary = false;

/** @var bool */
protected $_isFunctional = false;

/**
* Platform specific flags for indexes.
* array($flagName => true)
Expand Down Expand Up @@ -70,6 +74,10 @@ public function __construct(

foreach ($columns as $column) {
$this->_addColumn($column);

$this->_isFunctional = $this->_isFunctional === true
? $this->_isFunctional
: self::isFunctionalIndex($column);
}

foreach ($flags as $flag) {
Expand Down Expand Up @@ -104,10 +112,14 @@ public function getQuotedColumns(AbstractPlatform $platform)
foreach ($this->_columns as $column) {
$length = array_shift($subParts);

$quotedColumn = $column->getQuotedName($platform);
if ($this->isFunctional()) {
$quotedColumn = $column->getName();
} else {
$quotedColumn = $column->getQuotedName($platform);

if ($length !== null) {
$quotedColumn .= '(' . $length . ')';
if ($length !== null) {
$quotedColumn .= '(' . $length . ')';
}
}

$columns[] = $quotedColumn;
Expand Down Expand Up @@ -144,6 +156,11 @@ public function isPrimary()
return $this->_isPrimary;
}

public function isFunctional(): bool
{
return $this->_isFunctional;
}

/**
* @param string $name
* @param int $pos
Expand Down Expand Up @@ -334,6 +351,11 @@ public function getOptions()
return $this->options;
}

public static function isFunctionalIndex(string $name): bool
{
return $name[0] === '(' && substr($name, -1) === ')';
}

/**
* Return whether the two indexes have the same partial index
*/
Expand Down
20 changes: 18 additions & 2 deletions src/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
/**
* Schema manager for the MySQL RDBMS.
*
* @template T of AbstractMySQLPlatform
* @extends AbstractSchemaManager<AbstractMySQLPlatform>
*/
class MySQLSchemaManager extends AbstractSchemaManager
Expand Down Expand Up @@ -485,10 +486,12 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName =
$sql .= ' TABLE_NAME,';
}

$sql .= <<<'SQL'
$columnName = $this->getColumnNameForIndexFetch();

$sql .= <<<SQL
NON_UNIQUE AS Non_Unique,
INDEX_NAME AS Key_name,
COLUMN_NAME AS Column_Name,
{$columnName},
SUB_PART AS Sub_Part,
INDEX_TYPE AS Index_Type
FROM information_schema.STATISTICS
Expand Down Expand Up @@ -619,4 +622,17 @@ private function parseCreateOptions(?string $string): array

return $options;
}

/**
* EXPRESSION
*
* MySQL 8.0.13 and higher supports functional key parts (see Functional Key Parts), which affects both
* the COLUMN_NAME and EXPRESSION columns:
* For a nonfunctional key part, COLUMN_NAME indicates the column indexed by the key part and EXPRESSION is NULL.
* For a functional key part, COLUMN_NAME column is NULL and EXPRESSION indicates the expression for the key part.
*/
private function getColumnNameForIndexFetch(): string
{
return $this->_platform->getColumnNameForIndexFetch() . ' as Column_Name';
}
}
2 changes: 1 addition & 1 deletion src/Schema/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ private function _createIndex(
}

foreach ($columnNames as $columnName) {
if (! $this->hasColumn($columnName)) {
if (! $this->hasColumn($columnName) && ! Index::isFunctionalIndex($columnName)) {
throw SchemaException::columnDoesNotExist($columnName, $this->_name);
}
}
Expand Down
3 changes: 3 additions & 0 deletions tests/Driver/VersionAwarePlatformDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MariaDb1052Platform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL8013Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;
Expand Down Expand Up @@ -50,6 +51,8 @@ public static function mySQLVersionProvider(): array
['8', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
['8.0', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
['8.0.11', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
['8.0.13', MySQL8013Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
['8.0.14', MySQL8013Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
['6', MySQL57Platform::class],
['10.0.15-MariaDB-1~wheezy', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
['5.5.5-10.1.25-MariaDB', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
Expand Down
32 changes: 32 additions & 0 deletions tests/Functional/Schema/MySQLSchemaManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Doctrine\DBAL\Tests\Functional\Schema;

use DateTime;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
Expand Down Expand Up @@ -267,6 +268,37 @@ public function testColumnCharsetChange(): void
);
}

public function testCreateTableWithFunctionalIndex(): void
{
$tableName = 'table_with_functional_index';

if (! $this->connection->getDatabasePlatform()->supportsFunctionalIndex()) {
$this->expectException(Exception::class);
}

$table = new Table($tableName);
$table->addColumn('col_string', Types::STRING)
->setLength(100)
->setNotnull(true)
->setPlatformOption('charset', 'utf8');

$table->addIndex(['(LENGTH(col_string))'], 'length_index');

$this->dropAndCreateTable($table);

if (! ($this->connection->getDatabasePlatform()->supportsFunctionalIndex())) {
return;
}

$schema = $this->schemaManager->introspectTable($tableName);
self::assertArrayHasKey('length_index', $schema->getIndexes());
self::assertTrue($schema->getIndexes()['length_index']->isFunctional());
self::assertEquals(
'(length(`col_string`))',
$schema->getIndexes()['length_index']->getColumns()[0],
);
}

public function testColumnCollation(): void
{
$table = new Table('test_collation');
Expand Down
57 changes: 57 additions & 0 deletions tests/Schema/Platforms/MySQL8013SchemaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Doctrine\DBAL\Tests\Schema\Platforms;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQL8013Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Schema\Table;
use PHPUnit\Framework\TestCase;

class MySQL8013SchemaTest extends TestCase
{
private MySQL80Platform $platformMysql;
private MySQL8013Platform $platformMysql8013;

protected function setUp(): void
{
$this->platformMysql8013 = new MySQL8013Platform();
$this->platformMysql = new MySQL80Platform();
}

public function testGenerateFunctionalIndex(): void
{
$table = new Table('test');
$table->addColumn('foo_id', 'integer');
$table->addIndex(['foo_id', '(CAST(bar AS CHAR(10)))'], 'idx_foo_id');

$sqls = [];
foreach ($table->getIndexes() as $index) {
$sqls[] = $this->platformMysql8013->getCreateIndexSQL(
$index,
$table->getQuotedName($this->platformMysql8013),
);
}

self::assertEquals(
['CREATE INDEX idx_foo_id ON test (foo_id, (CAST(bar AS CHAR(10))))'],
$sqls,
);
}

public function testGenerateFunctionalIndexWithError(): void
{
$table = new Table('test');
$table->addColumn('foo_id', 'integer');
$table->addIndex(['foo_id', '(CAST(bar AS CHAR(10)))'], 'idx_foo_id');

foreach ($table->getIndexes() as $index) {
$this->expectException(Exception::class);

$this->platformMysql->getCreateIndexSQL(
$index,
$table->getQuotedName($this->platformMysql),
);
}
}
}