From 6484bf95fc5480c319610ec1e818c674178e7f81 Mon Sep 17 00:00:00 2001 From: Alexey Prohorov Date: Wed, 29 May 2024 18:13:15 +0300 Subject: [PATCH 1/2] Added support of functional indexes for MySQL and Postgres --- src/Driver/AbstractMySQLDriver.php | 11 ++ src/Platforms/AbstractMySQLPlatform.php | 5 + src/Platforms/AbstractPlatform.php | 38 +++++++ src/Platforms/MySQL8013Platform.php | 23 ++++ src/Platforms/MySQL84Platform.php | 2 +- src/Platforms/PostgreSQLPlatform.php | 5 + src/Platforms/SQLitePlatform.php | 11 ++ src/Schema/Index.php | 28 ++++- src/Schema/MySQLSchemaManager.php | 19 +++- src/Schema/PostgreSQLSchemaManager.php | 104 +++++++++--------- src/Schema/Table.php | 2 +- .../Driver/VersionAwarePlatformDriverTest.php | 3 + .../Platform/FunctionalIndexTest.php | 69 ++++++++++++ .../Schema/MySQLSchemaManagerTest.php | 32 ++++++ .../Schema/Platforms/MySQL8013SchemaTest.php | 59 ++++++++++ 15 files changed, 355 insertions(+), 56 deletions(-) create mode 100644 src/Platforms/MySQL8013Platform.php create mode 100644 tests/Functional/Platform/FunctionalIndexTest.php create mode 100644 tests/Schema/Platforms/MySQL8013SchemaTest.php diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index 5d33e04e4ad..77376b3cdfd 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -12,6 +12,7 @@ use Doctrine\DBAL\Platforms\MariaDB1052Platform; use Doctrine\DBAL\Platforms\MariaDB1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; +use Doctrine\DBAL\Platforms\MySQL8013Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQL84Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -58,7 +59,17 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs return new MySQL84Platform(); } + if (version_compare($version, '8.0.13', '>=')) { + return new MySQL8013Platform(); + } + if (version_compare($version, '8.0.0', '>=')) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6343', + 'Support for MySQL <= 8.0.13 is deprecated and will be removed in DBAL 5', + ); + return new MySQL80Platform(); } diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index 770d86341ef..4c72aca26dc 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -43,6 +43,11 @@ abstract class AbstractMySQLPlatform extends AbstractPlatform final public const LENGTH_LIMIT_BLOB = 65535; final public const LENGTH_LIMIT_MEDIUMBLOB = 16777215; + public function getColumnNameForIndexFetch(): string + { + return 'COLUMN_NAME'; + } + protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string { if ($limit !== null) { diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index b9c5fa2d19d..ae9c200c9b5 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -42,6 +42,7 @@ use function assert; use function count; use function explode; +use function get_debug_type; use function implode; use function in_array; use function is_array; @@ -794,6 +795,16 @@ private function buildCreateTableSQL(Table $table, bool $createForeignKeys): arr $options['primary'] = []; foreach ($table->getIndexes() as $index) { + if ($index->isFunctional() && ! $this->supportsFunctionalIndex()) { + throw new InvalidArgumentException(sprintf( + 'Index "%s" on table "%s" contains a functional part, ' . + 'but platform "%s" does not support functional indexes.', + $index->getName(), + $table->getName(), + get_debug_type($this), + )); + } + if (! $index->isPrimary()) { $options['indexes'][$index->getQuotedName($this)] = $index; @@ -1081,6 +1092,16 @@ public function getCreateIndexSQL(Index $index, string $table): string )); } + if ($index->isFunctional() && ! $this->supportsFunctionalIndex()) { + throw new InvalidArgumentException(sprintf( + 'Index "%s" on table "%s" contains a functional part, ' . + 'but platform "%s" does not support functional indexes.', + $name, + $table, + get_debug_type($this), + )); + } + if ($index->isPrimary()) { return $this->getCreatePrimaryKeySQL($index, $table); } @@ -1533,6 +1554,15 @@ public function getIndexDeclarationSQL(Index $index): string throw new InvalidArgumentException('Incomplete definition. "columns" required.'); } + if ($index->isFunctional() && ! $this->supportsFunctionalIndex()) { + throw new InvalidArgumentException(sprintf( + 'Index "%s" contains a functional part, ' . + 'but platform "%s" does not support functional indexes.', + $index->getName(), + get_debug_type($this), + )); + } + return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $index->getQuotedName($this) . ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index); } @@ -1973,6 +2003,14 @@ public function supportsColumnCollation(): bool return false; } + /** + * A flag that indicates whether the platform supports functional indexes. + */ + public function supportsFunctionalIndex(): bool + { + return false; + } + /** * Gets the format string, as accepted by the date() function, that describes * the format of a stored datetime value of this platform. diff --git a/src/Platforms/MySQL8013Platform.php b/src/Platforms/MySQL8013Platform.php new file mode 100644 index 00000000000..f75c5c3c9ca --- /dev/null +++ b/src/Platforms/MySQL8013Platform.php @@ -0,0 +1,23 @@ +isFunctional() && ! $this->supportsFunctionalIndex()) { + throw new InvalidArgumentException(sprintf( + 'Index "%s" on table "%s" contains a functional part, ' . + 'but platform "%s" does not support functional indexes.', + $name, + $table, + get_debug_type($this), + )); + } + if ($index->isPrimary()) { return $this->getCreatePrimaryKeySQL($index, $table); } diff --git a/src/Schema/Index.php b/src/Schema/Index.php index 175565483d1..9803f9c7676 100644 --- a/src/Schema/Index.php +++ b/src/Schema/Index.php @@ -12,6 +12,8 @@ use function array_search; use function array_shift; use function count; +use function str_ends_with; +use function str_starts_with; use function strtolower; class Index extends AbstractAsset @@ -27,6 +29,8 @@ class Index extends AbstractAsset protected bool $_isPrimary = false; + protected bool $_isFunctional = false; + /** * Platform specific flags for indexes. * @@ -58,6 +62,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) { @@ -101,10 +109,14 @@ public function getQuotedColumns(AbstractPlatform $platform): array 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; @@ -137,6 +149,11 @@ public function isPrimary(): bool return $this->_isPrimary; } + public function isFunctional(): bool + { + return $this->_isFunctional; + } + public function hasColumnAtPosition(string $name, int $pos = 0): bool { $name = $this->trimQuotes(strtolower($name)); @@ -283,6 +300,11 @@ public function getOptions(): array return $this->options; } + public static function isFunctionalIndex(string $name): bool + { + return str_starts_with($name, '(') && str_ends_with($name, ')'); + } + /** * Return whether the two indexes have the same partial index */ diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index fa042d653ce..40833e6314d 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -390,10 +390,12 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName = $sql .= ' TABLE_NAME,'; } - $sql .= <<<'SQL' + $columnName = $this->getColumnNameForIndexFetch(); + + $sql .= <<defaultTableOptions; } + + /** + * 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'; + } } diff --git a/src/Schema/PostgreSQLSchemaManager.php b/src/Schema/PostgreSQLSchemaManager.php index 9af16c9986a..59fccd8a655 100644 --- a/src/Schema/PostgreSQLSchemaManager.php +++ b/src/Schema/PostgreSQLSchemaManager.php @@ -19,14 +19,15 @@ use function implode; use function in_array; use function is_string; +use function json_decode; use function preg_match; -use function sprintf; use function str_contains; use function str_replace; use function strtolower; use function trim; use const CASE_LOWER; +use const JSON_THROW_ON_ERROR; /** * PostgreSQL Schema Manager. @@ -161,30 +162,16 @@ protected function _getPortableTableIndexesList(array $tableIndexes, string $tab { $buffer = []; foreach ($tableIndexes as $row) { - $colNumbers = array_map('intval', explode(' ', $row['indkey'])); - $columnNameSql = sprintf( - 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC', - $row['indrelid'], - implode(' ,', $colNumbers), - ); - - $indexColumns = $this->connection->fetchAllAssociative($columnNameSql); - - // required for getting the order of the columns right. - foreach ($colNumbers as $colNum) { - foreach ($indexColumns as $colRow) { - if ($colNum !== $colRow['attnum']) { - continue; - } - - $buffer[] = [ - 'key_name' => $row['relname'], - 'column_name' => trim($colRow['attname']), - 'non_unique' => ! $row['indisunique'], - 'primary' => $row['indisprimary'], - 'where' => $row['where'], - ]; - } + $indexColumns = json_decode($row['index_columns'], true, flags: JSON_THROW_ON_ERROR); + + foreach ($indexColumns as $colRow) { + $buffer[] = [ + 'key_name' => $row['relname'], + 'column_name' => trim($colRow), + 'non_unique' => ! $row['indisunique'], + 'primary' => $row['indisprimary'], + 'where' => $row['where'], + ]; } } @@ -468,34 +455,53 @@ protected function selectTableColumns(string $databaseName, ?string $tableName = protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result { - $sql = 'SELECT'; - + $tableNameSql = ''; if ($tableName === null) { - $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + $tableNameSql = <<<'SQL' + tc.relname AS table_name, + tn.nspname AS schema_name, + SQL; } - $sql .= <<<'SQL' - quote_ident(ic.relname) AS relname, - i.indisunique, - i.indisprimary, - i.indkey, - i.indrelid, - pg_get_expr(indpred, indrelid) AS "where" - FROM pg_index i - JOIN pg_class AS tc ON tc.oid = i.indrelid - JOIN pg_namespace tn ON tn.oid = tc.relnamespace - JOIN pg_class AS ic ON ic.oid = i.indexrelid - WHERE ic.oid IN ( - SELECT indexrelid - FROM pg_index i, pg_class c, pg_namespace n -SQL; - - $conditions = array_merge([ - 'c.oid = i.indrelid', - 'c.relnamespace = n.oid', - ], $this->buildQueryConditions($tableName)); + $whereConditions = array_merge( + [ + 'c.oid = i.indrelid', + 'c.relnamespace = n.oid', + ], + $this->buildQueryConditions($tableName), + ); - $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')'; + $whereSql = implode(' AND ', $whereConditions); + + $sql = <<connection->executeQuery($sql); } diff --git a/src/Schema/Table.php b/src/Schema/Table.php index cc7f04d2c49..d34b66ab169 100644 --- a/src/Schema/Table.php +++ b/src/Schema/Table.php @@ -743,7 +743,7 @@ private function _createIndex( } foreach ($columns as $columnName) { - if (! $this->hasColumn($columnName)) { + if (! $this->hasColumn($columnName) && ! Index::isFunctionalIndex($columnName)) { throw ColumnDoesNotExist::new($columnName, $this->_name); } } diff --git a/tests/Driver/VersionAwarePlatformDriverTest.php b/tests/Driver/VersionAwarePlatformDriverTest.php index 3996e4691d1..11478a0c58c 100644 --- a/tests/Driver/VersionAwarePlatformDriverTest.php +++ b/tests/Driver/VersionAwarePlatformDriverTest.php @@ -10,6 +10,7 @@ use Doctrine\DBAL\Platforms\MariaDB1052Platform; use Doctrine\DBAL\Platforms\MariaDB1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; +use Doctrine\DBAL\Platforms\MySQL8013Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQL84Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; @@ -37,6 +38,8 @@ public static function mySQLVersionProvider(): array return [ ['5.7.0', MySQLPlatform::class], ['8.0.11', MySQL80Platform::class], + ['8.0.13', MySQL8013Platform::class], + ['8.0.14', MySQL8013Platform::class], ['8.4.0', MySQL84Platform::class], ['5.5.40-MariaDB-1~wheezy', MariaDBPlatform::class], ['5.5.5-MariaDB-10.2.8+maria~xenial-log', MariaDBPlatform::class], diff --git a/tests/Functional/Platform/FunctionalIndexTest.php b/tests/Functional/Platform/FunctionalIndexTest.php new file mode 100644 index 00000000000..c142d8d2905 --- /dev/null +++ b/tests/Functional/Platform/FunctionalIndexTest.php @@ -0,0 +1,69 @@ +connection->getDatabasePlatform(); + if (! $platform->supportsFunctionalIndex()) { + self::markTestSkipped('Platform does not support functional indexes.'); + } + + $tableName = 'some_table'; + + $table = new Table($tableName); + $table->addColumn('column1', Types::INTEGER, ['notnull' => false]); + $table->addColumn('column2', Types::INTEGER, ['notnull' => false]); + $table->addIndex(['column1', 'column2', '(column2 IS NOT NULL)'], 'func_idx'); + $this->dropAndCreateTable($table); + + $this->connection->insert($tableName, ['column1' => 1]); + + $tablesFromList = $this->connection->createSchemaManager()->listTables(); + + $tables = array_filter($tablesFromList, static fn (Table $table): bool => $table->getName() === $tableName); + $someTable = array_pop($tables); + + self::assertInstanceOf(Table::class, $someTable); + self::assertEquals($tableName, $someTable->getName()); + + $index = $someTable->getIndex('func_idx'); + + self::assertTrue($index->isFunctional()); + + if (TestUtil::isDriverOneOf('pdo_pgsql', 'pgsql')) { + self::assertEquals(['column1', 'column2', '(column2 IS NOT NULL)'], $index->getColumns()); + } else { + self::assertEquals(['column1', 'column2', '((`column2` is not null))'], $index->getColumns()); + } + } + + public function testPlatformException(): void + { + $platform = $this->connection->getDatabasePlatform(); + if ($platform->supportsFunctionalIndex()) { + self::markTestSkipped('Skipping, platform supports functional indexes.'); + } + + $table = new Table('some_table'); + $table->addColumn('column1', Types::INTEGER, ['notnull' => false]); + $table->addColumn('column2', Types::INTEGER, ['notnull' => false]); + $table->addIndex(['column1', 'column2', '(column2 IS NOT NULL)'], 'func_idx'); + + $this->expectException(InvalidArgumentException::class); + $this->dropAndCreateTable($table); + } +} diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 2b17b6a25be..5502e1af8fc 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -6,6 +6,7 @@ use DateTime; use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\DatabaseRequired; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; @@ -250,6 +251,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'); diff --git a/tests/Schema/Platforms/MySQL8013SchemaTest.php b/tests/Schema/Platforms/MySQL8013SchemaTest.php new file mode 100644 index 00000000000..fe50f64aca6 --- /dev/null +++ b/tests/Schema/Platforms/MySQL8013SchemaTest.php @@ -0,0 +1,59 @@ +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), + ); + } + } +} From 3fba75964e899a0ad5c58424d58c13ee97908cd6 Mon Sep 17 00:00:00 2001 From: Aleksei Prokhorov Date: Thu, 27 Jun 2024 17:56:12 +0300 Subject: [PATCH 2/2] Code review changes --- src/Driver/AbstractMySQLDriver.php | 6 ++-- src/Platforms/AbstractMySQLPlatform.php | 2 +- src/Platforms/AbstractPlatform.php | 2 +- src/Platforms/MariaDBPlatform.php | 5 +++ src/Platforms/MySQL8013Platform.php | 17 ++++++---- src/Platforms/MySQL80Platform.php | 5 +++ src/Schema/Index.php | 12 ++++--- src/Schema/MySQLSchemaManager.php | 2 +- src/Schema/Table.php | 2 +- .../Platform/FunctionalIndexTest.php | 24 +++++--------- .../Schema/MySQLSchemaManagerTest.php | 33 ++++++++++++------- .../Schema/Platforms/MySQL8013SchemaTest.php | 21 +++++------- 12 files changed, 72 insertions(+), 59 deletions(-) diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php index 77376b3cdfd..597623261cd 100644 --- a/src/Driver/AbstractMySQLDriver.php +++ b/src/Driver/AbstractMySQLDriver.php @@ -48,7 +48,7 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6343', + 'https://github.com/doctrine/dbal/pull/6414', 'Support for MariaDB < 10.5.2 is deprecated and will be removed in DBAL 5', ); @@ -66,7 +66,7 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs if (version_compare($version, '8.0.0', '>=')) { Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6343', + 'https://github.com/doctrine/dbal/pull/6414', 'Support for MySQL <= 8.0.13 is deprecated and will be removed in DBAL 5', ); @@ -75,7 +75,7 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs Deprecation::trigger( 'doctrine/dbal', - 'https://github.com/doctrine/dbal/pull/6343', + 'https://github.com/doctrine/dbal/pull/6414', 'Support for MySQL < 8 is deprecated and will be removed in DBAL 5', ); diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index 4c72aca26dc..1844151478a 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -43,7 +43,7 @@ abstract class AbstractMySQLPlatform extends AbstractPlatform final public const LENGTH_LIMIT_BLOB = 65535; final public const LENGTH_LIMIT_MEDIUMBLOB = 16777215; - public function getColumnNameForIndexFetch(): string + public function getColumnOrExpressionNameForIndexFetching(): string { return 'COLUMN_NAME'; } diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index ae9c200c9b5..94aee4f3ce3 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -2008,7 +2008,7 @@ public function supportsColumnCollation(): bool */ public function supportsFunctionalIndex(): bool { - return false; + return true; } /** diff --git a/src/Platforms/MariaDBPlatform.php b/src/Platforms/MariaDBPlatform.php index d4082aef0f6..011b325af67 100644 --- a/src/Platforms/MariaDBPlatform.php +++ b/src/Platforms/MariaDBPlatform.php @@ -162,4 +162,9 @@ protected function createReservedKeywordsList(): KeywordList { return new MariaDBKeywords(); } + + public function supportsFunctionalIndex(): bool + { + return false; + } } diff --git a/src/Platforms/MySQL8013Platform.php b/src/Platforms/MySQL8013Platform.php index f75c5c3c9ca..2226b7d7198 100644 --- a/src/Platforms/MySQL8013Platform.php +++ b/src/Platforms/MySQL8013Platform.php @@ -11,13 +11,16 @@ */ class MySQL8013Platform extends MySQLPlatform { - public function getColumnNameForIndexFetch(): string + public function getColumnOrExpressionNameForIndexFetching(): string { - return "COALESCE(COLUMN_NAME, CONCAT('(', REPLACE(EXPRESSION, '\\\''', ''''), ')'))"; - } - - public function supportsFunctionalIndex(): bool - { - return true; + return <<<'SQL' + COALESCE( + COLUMN_NAME, + (CASE WHEN SUBSTR(EXPRESSION, 1, 1) != '(' + THEN CONCAT('(', REPLACE(EXPRESSION, '\'', ''''), ')') + ELSE REPLACE(EXPRESSION, '\'', '''') + END) + ) + SQL; } } diff --git a/src/Platforms/MySQL80Platform.php b/src/Platforms/MySQL80Platform.php index f5517e97789..7d3ac832936 100644 --- a/src/Platforms/MySQL80Platform.php +++ b/src/Platforms/MySQL80Platform.php @@ -24,4 +24,9 @@ public function createSelectSQLBuilder(): SelectSQLBuilder { return AbstractPlatform::createSelectSQLBuilder(); } + + public function supportsFunctionalIndex(): bool + { + return false; + } } diff --git a/src/Schema/Index.php b/src/Schema/Index.php index 9803f9c7676..dd7dc5dbb86 100644 --- a/src/Schema/Index.php +++ b/src/Schema/Index.php @@ -63,9 +63,11 @@ public function __construct( foreach ($columns as $column) { $this->_addColumn($column); - $this->_isFunctional = $this->_isFunctional === true - ? $this->_isFunctional - : self::isFunctionalIndex($column); + if ($this->_isFunctional === true) { + continue; + } + + $this->_isFunctional = self::isColumnNameAnExpression($column); } foreach ($flags as $flag) { @@ -300,9 +302,9 @@ public function getOptions(): array return $this->options; } - public static function isFunctionalIndex(string $name): bool + public static function isColumnNameAnExpression(string $columnName): bool { - return str_starts_with($name, '(') && str_ends_with($name, ')'); + return str_starts_with($columnName, '(') && str_ends_with($columnName, ')'); } /** diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index 40833e6314d..c0b9be4c474 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -552,6 +552,6 @@ private function getDefaultTableOptions(): DefaultTableOptions */ private function getColumnNameForIndexFetch(): string { - return $this->platform->getColumnNameForIndexFetch() . ' as Column_Name'; + return $this->platform->getColumnOrExpressionNameForIndexFetching() . ' as Column_Name'; } } diff --git a/src/Schema/Table.php b/src/Schema/Table.php index d34b66ab169..a4db52b0e03 100644 --- a/src/Schema/Table.php +++ b/src/Schema/Table.php @@ -743,7 +743,7 @@ private function _createIndex( } foreach ($columns as $columnName) { - if (! $this->hasColumn($columnName) && ! Index::isFunctionalIndex($columnName)) { + if (! $this->hasColumn($columnName) && ! Index::isColumnNameAnExpression($columnName)) { throw ColumnDoesNotExist::new($columnName, $this->_name); } } diff --git a/tests/Functional/Platform/FunctionalIndexTest.php b/tests/Functional/Platform/FunctionalIndexTest.php index c142d8d2905..16f1b9f9b32 100644 --- a/tests/Functional/Platform/FunctionalIndexTest.php +++ b/tests/Functional/Platform/FunctionalIndexTest.php @@ -5,14 +5,12 @@ namespace Doctrine\DBAL\Tests\Functional\Platform; use Doctrine\DBAL\Exception\InvalidArgumentException; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Tests\FunctionalTestCase; -use Doctrine\DBAL\Tests\TestUtil; use Doctrine\DBAL\Types\Types; -use function array_filter; -use function array_pop; - class FunctionalIndexTest extends FunctionalTestCase { public function testGetIndex(): void @@ -22,7 +20,7 @@ public function testGetIndex(): void self::markTestSkipped('Platform does not support functional indexes.'); } - $tableName = 'some_table'; + $tableName = 'functional_index_table'; $table = new Table($tableName); $table->addColumn('column1', Types::INTEGER, ['notnull' => false]); @@ -32,22 +30,16 @@ public function testGetIndex(): void $this->connection->insert($tableName, ['column1' => 1]); - $tablesFromList = $this->connection->createSchemaManager()->listTables(); - - $tables = array_filter($tablesFromList, static fn (Table $table): bool => $table->getName() === $tableName); - $someTable = array_pop($tables); - - self::assertInstanceOf(Table::class, $someTable); - self::assertEquals($tableName, $someTable->getName()); + $functionalIndexTable = $this->connection->createSchemaManager()->introspectTable($tableName); - $index = $someTable->getIndex('func_idx'); + $index = $functionalIndexTable->getIndex('func_idx'); self::assertTrue($index->isFunctional()); - if (TestUtil::isDriverOneOf('pdo_pgsql', 'pgsql')) { + if ($platform instanceof PostgreSQLPlatform) { self::assertEquals(['column1', 'column2', '(column2 IS NOT NULL)'], $index->getColumns()); - } else { - self::assertEquals(['column1', 'column2', '((`column2` is not null))'], $index->getColumns()); + } elseif ($platform instanceof MySQLPlatform) { + self::assertEquals(['column1', 'column2', '(`column2` is not null)'], $index->getColumns()); } } diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 5502e1af8fc..40521d76a71 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -251,33 +251,44 @@ public function testColumnCharsetChange(): void ); } + public function testCreateTableWithFunctionalIndexWithUnsupportedPlatform(): void + { + if ($this->connection->getDatabasePlatform()->supportsFunctionalIndex()) { + self::markTestSkipped('This test is only for platforms that do not support Functional Indexes'); + } + + $this->expectException(Exception::class); + + $table = new Table('table_with_functional_index'); + $table->addColumn('string_column', Types::STRING) + ->setLength(100) + ->setNotnull(true) + ->setPlatformOption('charset', 'utf8'); + + $table->addIndex(['(LENGTH(string_column))'], 'length_index'); + + $this->dropAndCreateTable($table); + } + 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) + $table->addColumn('string_column', Types::STRING) ->setLength(100) ->setNotnull(true) ->setPlatformOption('charset', 'utf8'); - $table->addIndex(['(LENGTH(col_string))'], 'length_index'); + $table->addIndex(['(LENGTH(string_column))'], '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`))', + '(length(`string_column`))', $schema->getIndexes()['length_index']->getColumns()[0], ); } diff --git a/tests/Schema/Platforms/MySQL8013SchemaTest.php b/tests/Schema/Platforms/MySQL8013SchemaTest.php index fe50f64aca6..f5a9ff1ceb3 100644 --- a/tests/Schema/Platforms/MySQL8013SchemaTest.php +++ b/tests/Schema/Platforms/MySQL8013SchemaTest.php @@ -12,26 +12,19 @@ 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'); + $platform = new MySQL8013Platform(); + $sqls = []; foreach ($table->getIndexes() as $index) { - $sqls[] = $this->platformMysql8013->getCreateIndexSQL( + $sqls[] = $platform->getCreateIndexSQL( $index, - $table->getQuotedName($this->platformMysql8013), + $table->getQuotedName($platform), ); } @@ -47,12 +40,14 @@ public function testGenerateFunctionalIndexWithError(): void $table->addColumn('foo_id', 'integer'); $table->addIndex(['foo_id', '(CAST(bar AS CHAR(10)))'], 'idx_foo_id'); + $platform = new MySQL80Platform(); + foreach ($table->getIndexes() as $index) { $this->expectException(Exception::class); - $this->platformMysql->getCreateIndexSQL( + $platform->getCreateIndexSQL( $index, - $table->getQuotedName($this->platformMysql), + $table->getQuotedName($platform), ); } }