Skip to content

Commit

Permalink
Added support of functional indexes for MySQL and Postgres
Browse files Browse the repository at this point in the history
  • Loading branch information
prohalexey committed Jun 6, 2024
1 parent 754e3ee commit dfef783
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 55 deletions.
5 changes: 5 additions & 0 deletions src/Driver/AbstractMySQLDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +59,10 @@ public function getDatabasePlatform(ServerVersionProvider $versionProvider): Abs
return new MySQL84Platform();
}

if (version_compare($version, '8.0.13', '>=')) {
return new MySQL8013Platform();

Check failure on line 63 in src/Driver/AbstractMySQLDriver.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.3)

DeprecatedClass

src/Driver/AbstractMySQLDriver.php:63:20: DeprecatedClass: Doctrine\DBAL\Platforms\MySQL8013Platform is marked deprecated (see https://psalm.dev/098)
}

if (version_compare($version, '8.0.0', '>=')) {
return new MySQL80Platform();
}
Expand Down
5 changes: 5 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 46 in src/Platforms/AbstractMySQLPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractMySQLPlatform.php#L46

Added line #L46 was not covered by tests
{
return 'COLUMN_NAME';

Check warning on line 48 in src/Platforms/AbstractMySQLPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractMySQLPlatform.php#L48

Added line #L48 was not covered by tests
}

protected function doModifyLimitQuery(string $query, ?int $limit, int $offset): string
{
if ($limit !== null) {
Expand Down
39 changes: 39 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
use function str_contains;
use function str_replace;
use function strlen;
use function strrpos;

Check failure on line 58 in src/Platforms/AbstractPlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Type strrpos is not used in this file.
use function strtolower;
use function strtoupper;
use function substr;

Check failure on line 61 in src/Platforms/AbstractPlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Type substr is not used in this file.

/**
* Base class for all DatabasePlatforms. The DatabasePlatforms are the central
Expand Down Expand Up @@ -794,6 +796,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),

Check failure on line 805 in src/Platforms/AbstractPlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Function get_debug_type() should not be referenced via a fallback global name, but via a use statement.
));
}

if (! $index->isPrimary()) {
$options['indexes'][$index->getQuotedName($this)] = $index;

Expand Down Expand Up @@ -1081,6 +1093,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),

Check failure on line 1102 in src/Platforms/AbstractPlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Function get_debug_type() should not be referenced via a fallback global name, but via a use statement.
));
}

if ($index->isPrimary()) {
return $this->getCreatePrimaryKeySQL($index, $table);
}
Expand Down Expand Up @@ -1533,6 +1555,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),

Check failure on line 1563 in src/Platforms/AbstractPlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Function get_debug_type() should not be referenced via a fallback global name, but via a use statement.
));

Check warning on line 1564 in src/Platforms/AbstractPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractPlatform.php#L1559-L1564

Added lines #L1559 - L1564 were not covered by tests
}

return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $index->getQuotedName($this)
. ' (' . implode(', ', $index->getQuotedColumns($this)) . ')' . $this->getPartialIndexSQL($index);
}
Expand Down Expand Up @@ -1973,6 +2004,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.
Expand Down
24 changes: 24 additions & 0 deletions src/Platforms/MySQL8013Platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

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.
* @deprecated This class will be removed once support for MySQL 8.0.13 is dropped.

Check failure on line 11 in src/Platforms/MySQL8013Platform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Expected 1 line between description and annotations, found 0.
*/
class MySQL8013Platform extends MySQL80Platform
{
public function getColumnNameForIndexFetch(): string

Check warning on line 15 in src/Platforms/MySQL8013Platform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/MySQL8013Platform.php#L15

Added line #L15 was not covered by tests
{
return "COALESCE(COLUMN_NAME, CONCAT('(', REPLACE(EXPRESSION, '\\\''', ''''), ')'))";

Check warning on line 17 in src/Platforms/MySQL8013Platform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/MySQL8013Platform.php#L17

Added line #L17 was not covered by tests
}

public function supportsFunctionalIndex(): bool
{
return true;
}
}
5 changes: 5 additions & 0 deletions src/Platforms/MySQL84Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ protected function createReservedKeywordsList(): KeywordList
{
return new MySQL84Keywords();
}

public function supportsFunctionalIndex(): bool

Check warning on line 20 in src/Platforms/MySQL84Platform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/MySQL84Platform.php#L20

Added line #L20 was not covered by tests
{
return true;

Check warning on line 22 in src/Platforms/MySQL84Platform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/MySQL84Platform.php#L22

Added line #L22 was not covered by tests
}
}
5 changes: 5 additions & 0 deletions src/Platforms/PostgreSQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -781,4 +781,9 @@ public function createSchemaManager(Connection $connection): PostgreSQLSchemaMan
{
return new PostgreSQLSchemaManager($connection, $this);
}

public function supportsFunctionalIndex(): bool

Check warning on line 785 in src/Platforms/PostgreSQLPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/PostgreSQLPlatform.php#L785

Added line #L785 was not covered by tests
{
return true;

Check warning on line 787 in src/Platforms/PostgreSQLPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/PostgreSQLPlatform.php#L787

Added line #L787 was not covered by tests
}
}
11 changes: 11 additions & 0 deletions src/Platforms/SQLitePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;

Check failure on line 38 in src/Platforms/SQLitePlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Type strrpos is not used in this file.
use function strtolower;
use function substr;
use function trim;
Expand Down Expand Up @@ -561,6 +562,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),

Check failure on line 571 in src/Platforms/SQLitePlatform.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (8.3)

Function get_debug_type() should not be referenced via a fallback global name, but via a use statement.
));

Check warning on line 572 in src/Platforms/SQLitePlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/SQLitePlatform.php#L566-L572

Added lines #L566 - L572 were not covered by tests
}

if ($index->isPrimary()) {
return $this->getCreatePrimaryKeySQL($index, $table);
}
Expand Down
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,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
Expand All @@ -27,6 +29,8 @@ class Index extends AbstractAsset

protected bool $_isPrimary = false;

protected bool $_isFunctional = false;

/**
* Platform specific flags for indexes.
*
Expand Down Expand Up @@ -58,6 +62,10 @@ public function __construct(

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

$this->_isFunctional = $this->_isFunctional === true
? $this->_isFunctional

Check warning on line 67 in src/Schema/Index.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/Index.php#L67

Added line #L67 was not covered by tests
: self::isFunctionalIndex($column);
}

foreach ($flags as $flag) {
Expand Down Expand Up @@ -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 . ')';

Check warning on line 118 in src/Schema/Index.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/Index.php#L118

Added line #L118 was not covered by tests
}
}

$columns[] = $quotedColumn;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
*/
Expand Down
19 changes: 17 additions & 2 deletions src/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,12 @@ protected function selectIndexColumns(string $databaseName, ?string $tableName =
$sql .= ' TABLE_NAME,';
}

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

Check warning on line 393 in src/Schema/MySQLSchemaManager.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/MySQLSchemaManager.php#L393

Added line #L393 was not covered by tests

$sql .= <<<SQL

Check warning on line 395 in src/Schema/MySQLSchemaManager.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/MySQLSchemaManager.php#L395

Added line #L395 was not covered by tests
NON_UNIQUE AS Non_Unique,
INDEX_NAME AS Key_name,
COLUMN_NAME AS Column_Name,
{$columnName},

Check warning on line 398 in src/Schema/MySQLSchemaManager.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/MySQLSchemaManager.php#L398

Added line #L398 was not covered by tests
SUB_PART AS Sub_Part,
INDEX_TYPE AS Index_Type
FROM information_schema.STATISTICS
Expand Down Expand Up @@ -539,4 +541,17 @@ private function getDefaultTableOptions(): DefaultTableOptions

return $this->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

Check warning on line 553 in src/Schema/MySQLSchemaManager.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/MySQLSchemaManager.php#L553

Added line #L553 was not covered by tests
{
return $this->platform->getColumnNameForIndexFetch() . ' as Column_Name';

Check warning on line 555 in src/Schema/MySQLSchemaManager.php

View check run for this annotation

Codecov / codecov/patch

src/Schema/MySQLSchemaManager.php#L555

Added line #L555 was not covered by tests
}
}
Loading

0 comments on commit dfef783

Please sign in to comment.